@things-factory/figure-ui 10.1.81 → 10.1.83
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/client/figure-assistant.ts +2 -2
- package/client/graphql/index.ts +99 -10
- package/client/graphql/reached.ts +25 -0
- package/client/graphql/store.ts +27 -4
- package/client/modeller/ai-proposal.ts +74 -9
- package/client/modeller/align.ts +2 -3
- package/client/modeller/canvas.ts +18 -5
- package/client/modeller/command-preview.ts +113 -0
- package/client/modeller/editor.ts +800 -53
- package/client/modeller/fields.ts +12 -4
- package/client/modeller/figure-facts.ts +2 -2
- package/client/modeller/figure-first-save.ts +36 -14
- package/client/modeller/figure-history-panel.ts +133 -14
- package/client/modeller/figure-links.ts +357 -94
- package/client/modeller/figure-parts.ts +56 -2
- package/client/modeller/figure-preview.ts +19 -1
- package/client/modeller/figure-properties.ts +3 -3
- package/client/modeller/figure-revert-choice.ts +49 -0
- package/client/modeller/first-save.ts +16 -0
- package/client/modeller/gizmo.ts +0 -16
- package/client/modeller/links.ts +142 -15
- package/client/modeller/motion-ends.ts +83 -0
- package/client/modeller/occupancy.ts +28 -2
- package/client/modeller/product-session.ts +154 -18
- package/client/modeller/refusals.ts +15 -0
- package/client/modeller/revert-request.ts +26 -0
- package/client/modeller/side-panel-styles.ts +12 -0
- package/client/modeller/stage-model.ts +25 -3
- package/client/pages/figure-modeller-page.ts +68 -29
- package/dist-client/figure-assistant.js +2 -2
- package/dist-client/figure-assistant.js.map +1 -1
- package/dist-client/graphql/index.d.ts +39 -2
- package/dist-client/graphql/index.js +63 -9
- package/dist-client/graphql/index.js.map +1 -1
- package/dist-client/graphql/reached.d.ts +10 -0
- package/dist-client/graphql/reached.js +28 -0
- package/dist-client/graphql/reached.js.map +1 -0
- package/dist-client/graphql/store.js +27 -4
- package/dist-client/graphql/store.js.map +1 -1
- package/dist-client/modeller/ai-proposal.d.ts +31 -2
- package/dist-client/modeller/ai-proposal.js +65 -6
- package/dist-client/modeller/ai-proposal.js.map +1 -1
- package/dist-client/modeller/align.d.ts +1 -1
- package/dist-client/modeller/align.js +0 -2
- package/dist-client/modeller/align.js.map +1 -1
- package/dist-client/modeller/canvas.d.ts +11 -0
- package/dist-client/modeller/canvas.js +33 -4
- package/dist-client/modeller/canvas.js.map +1 -1
- package/dist-client/modeller/command-preview.d.ts +35 -0
- package/dist-client/modeller/command-preview.js +95 -0
- package/dist-client/modeller/command-preview.js.map +1 -0
- package/dist-client/modeller/editor.d.ts +107 -1
- package/dist-client/modeller/editor.js +823 -49
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/fields.d.ts +3 -2
- package/dist-client/modeller/fields.js +10 -3
- package/dist-client/modeller/fields.js.map +1 -1
- package/dist-client/modeller/figure-capability-panel.d.ts +1 -1
- package/dist-client/modeller/figure-clips.d.ts +1 -1
- package/dist-client/modeller/figure-controls.d.ts +1 -1
- package/dist-client/modeller/figure-facts.js +2 -2
- package/dist-client/modeller/figure-facts.js.map +1 -1
- package/dist-client/modeller/figure-first-save.d.ts +3 -4
- package/dist-client/modeller/figure-first-save.js +41 -14
- package/dist-client/modeller/figure-first-save.js.map +1 -1
- package/dist-client/modeller/figure-history-panel.d.ts +31 -0
- package/dist-client/modeller/figure-history-panel.js +130 -14
- package/dist-client/modeller/figure-history-panel.js.map +1 -1
- package/dist-client/modeller/figure-links.d.ts +52 -2
- package/dist-client/modeller/figure-links.js +366 -97
- package/dist-client/modeller/figure-links.js.map +1 -1
- package/dist-client/modeller/figure-material-panel.d.ts +1 -1
- package/dist-client/modeller/figure-parts.d.ts +7 -0
- package/dist-client/modeller/figure-parts.js +55 -2
- package/dist-client/modeller/figure-parts.js.map +1 -1
- package/dist-client/modeller/figure-preview.d.ts +6 -0
- package/dist-client/modeller/figure-preview.js +22 -1
- package/dist-client/modeller/figure-preview.js.map +1 -1
- package/dist-client/modeller/figure-properties.d.ts +1 -1
- package/dist-client/modeller/figure-properties.js +3 -3
- package/dist-client/modeller/figure-properties.js.map +1 -1
- package/dist-client/modeller/figure-revert-choice.d.ts +17 -0
- package/dist-client/modeller/figure-revert-choice.js +59 -0
- package/dist-client/modeller/figure-revert-choice.js.map +1 -0
- package/dist-client/modeller/first-save.d.ts +7 -0
- package/dist-client/modeller/first-save.js +17 -0
- package/dist-client/modeller/first-save.js.map +1 -1
- package/dist-client/modeller/gizmo.d.ts +0 -6
- package/dist-client/modeller/gizmo.js +0 -18
- package/dist-client/modeller/gizmo.js.map +1 -1
- package/dist-client/modeller/links.d.ts +49 -4
- package/dist-client/modeller/links.js +141 -15
- package/dist-client/modeller/links.js.map +1 -1
- package/dist-client/modeller/motion-ends.d.ts +12 -0
- package/dist-client/modeller/motion-ends.js +65 -0
- package/dist-client/modeller/motion-ends.js.map +1 -0
- package/dist-client/modeller/occupancy.d.ts +14 -1
- package/dist-client/modeller/occupancy.js +14 -1
- package/dist-client/modeller/occupancy.js.map +1 -1
- package/dist-client/modeller/product-session.d.ts +43 -2
- package/dist-client/modeller/product-session.js +146 -12
- package/dist-client/modeller/product-session.js.map +1 -1
- package/dist-client/modeller/refusals.d.ts +13 -0
- package/dist-client/modeller/refusals.js +16 -0
- package/dist-client/modeller/refusals.js.map +1 -1
- package/dist-client/modeller/revert-request.d.ts +16 -0
- package/dist-client/modeller/revert-request.js +11 -0
- package/dist-client/modeller/revert-request.js.map +1 -0
- package/dist-client/modeller/side-panel-styles.js +12 -0
- package/dist-client/modeller/side-panel-styles.js.map +1 -1
- package/dist-client/modeller/stage-model.d.ts +18 -2
- package/dist-client/modeller/stage-model.js +21 -3
- package/dist-client/modeller/stage-model.js.map +1 -1
- package/dist-client/pages/figure-modeller-page.d.ts +8 -1
- package/dist-client/pages/figure-modeller-page.js +72 -34
- package/dist-client/pages/figure-modeller-page.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/test/ai-proposal.test.ts +2 -2
- package/test/align.test.ts +50 -5
- package/test/browser/acceptance-page.js +187 -0
- package/test/browser/build.mjs +5 -1
- package/test/browser/revert-page.js +47 -0
- package/test/browser/serve-acceptance.mjs +85 -0
- package/test/browser/serve-revert.mjs +119 -0
- package/test/clip-ai-proposal.test.ts +104 -0
- package/test/clip-consent.test.ts +88 -0
- package/test/command-preview.test.ts +288 -0
- package/test/fields.test.ts +4 -3
- package/test/figure-assistant-surface.test.ts +4 -1
- package/test/first-minute.test.ts +96 -0
- package/test/first-save-suggest.test.ts +42 -0
- package/test/gizmo.test.ts +20 -16
- package/test/joint-fan.test.ts +2 -1
- package/test/layout.test.ts +9 -2
- package/test/links.test.ts +9 -5
- package/test/proposal-record.test.ts +126 -0
- package/test/release-entry.test.ts +28 -0
- package/test/release-panel.test.ts +3 -2
- package/test/resize-with-placement.test.ts +226 -0
- package/test/revert-choice.test.ts +39 -0
- package/test/revert-keeping.test.ts +102 -0
- package/test/save-landed.test.ts +198 -0
- package/translations/en.json +229 -11
- package/translations/ja.json +229 -11
- package/translations/ko.json +230 -12
- package/translations/ms.json +228 -10
- package/translations/zh.json +229 -11
|
@@ -7,9 +7,16 @@ import {
|
|
|
7
7
|
type FigurePrimitiveCreation,
|
|
8
8
|
type FigurePartEdit,
|
|
9
9
|
type FigureAuthoringAction,
|
|
10
|
-
type FigureProposalStep
|
|
10
|
+
type FigureProposalStep,
|
|
11
|
+
type FigureClipConsent
|
|
11
12
|
} from '@hatiolab/figure-model'
|
|
12
13
|
|
|
14
|
+
/** Consent to let clips go, and the figure and version it was given on. */
|
|
15
|
+
export interface FigureClipConsentFor extends FigureClipConsent {
|
|
16
|
+
context: string
|
|
17
|
+
revision: number
|
|
18
|
+
}
|
|
19
|
+
|
|
13
20
|
export interface FigureProductRecord {
|
|
14
21
|
id: string
|
|
15
22
|
source: string
|
|
@@ -18,6 +25,8 @@ export interface FigureProductRecord {
|
|
|
18
25
|
/** Persistence is injected; neither the model engine nor the renderer owns the product ID. */
|
|
19
26
|
export interface FigureProductStore {
|
|
20
27
|
load(id: string): Promise<FigureProductRecord>
|
|
28
|
+
/** Every field a save writes, as the server holds it now: to tell whether a save whose answer was lost landed. */
|
|
29
|
+
readBack?(id: string): Promise<FigureProductRecord & { thumbnail?: string; type?: string }>
|
|
21
30
|
save(request: {
|
|
22
31
|
id?: string
|
|
23
32
|
source: string
|
|
@@ -27,6 +36,8 @@ export interface FigureProductStore {
|
|
|
27
36
|
thumbnail?: string
|
|
28
37
|
/** The type name a new figure is created under (the first save asks for it). Not sent for an existing one. */
|
|
29
38
|
type?: string
|
|
39
|
+
/** Keys of the AI proposals whose steps the source holds: the server records them with the save. */
|
|
40
|
+
proposalKeys?: string[]
|
|
30
41
|
}): Promise<FigureProductRecord>
|
|
31
42
|
}
|
|
32
43
|
|
|
@@ -40,9 +51,47 @@ export function createFigureProductSession(store: FigureProductStore) {
|
|
|
40
51
|
saving = false,
|
|
41
52
|
error: string | undefined
|
|
42
53
|
let creationId: string | undefined
|
|
54
|
+
/*
|
|
55
|
+
The history as steps, each tagged with the AI proposal it came from (or nothing): mirrored here, since the
|
|
56
|
+
workspace's history does not say where a step came from. A proposal is in the model while its step is on `done` --
|
|
57
|
+
undone, it is not; undone and written past, it never comes back. What a save sends as `proposalKeys`.
|
|
58
|
+
*/
|
|
59
|
+
let done: Array<string | undefined> = [],
|
|
60
|
+
undone: Array<string | undefined> = []
|
|
61
|
+
function tracked<T>(kind: 'do' | 'undo' | 'redo', run: () => T, tag?: string): T {
|
|
62
|
+
const before = workspace.read().revision
|
|
63
|
+
const result = run()
|
|
64
|
+
if (workspace.read().revision === before) return result
|
|
65
|
+
if (kind === 'do') {
|
|
66
|
+
done.push(tag)
|
|
67
|
+
undone = []
|
|
68
|
+
} else if (kind === 'undo') undone.push(done.pop())
|
|
69
|
+
else done.push(undone.pop())
|
|
70
|
+
return result
|
|
71
|
+
}
|
|
72
|
+
const inEffect = () => [...new Set(done.filter((tag): tag is string => !!tag))]
|
|
43
73
|
function read() {
|
|
44
74
|
return { ...workspace.read(), id, loading, saving, error }
|
|
45
75
|
}
|
|
76
|
+
/* Texts sent whose save was never answered: found on the server, the server is confirmed to hold what was sent. */
|
|
77
|
+
const unconfirmed = new Map<string, { thumbnail?: string }>()
|
|
78
|
+
/*
|
|
79
|
+
Consent to let clips go: given for one edit, on the figure and the version it was asked on. The first edit made under
|
|
80
|
+
it takes it; an edit on another figure or another version is refused rather than let clips go that were never named
|
|
81
|
+
for it (designer 2026-09-27).
|
|
82
|
+
*/
|
|
83
|
+
let consent: FigureClipConsentFor | undefined
|
|
84
|
+
const consented = () => {
|
|
85
|
+
if (!consent) return {}
|
|
86
|
+
const given = consent
|
|
87
|
+
consent = undefined
|
|
88
|
+
const view = workspace.read()
|
|
89
|
+
if (view.context !== given.context || view.revision !== given.revision) {
|
|
90
|
+
const stale = Object.assign(Error('확인한 뒤 모델이 바뀌어 적용하지 않았습니다. 다시 해 주세요.'), { code: 'CONSENT_STALE' })
|
|
91
|
+
throw stale
|
|
92
|
+
}
|
|
93
|
+
return { dropClips: given.dropClips }
|
|
94
|
+
}
|
|
46
95
|
function writable() {
|
|
47
96
|
const view = workspace.read()
|
|
48
97
|
if (loading || error || !view.context || view.revision === null) throw Error('먼저 모델을 정상적으로 열어 주세요.')
|
|
@@ -57,6 +106,8 @@ export function createFigureProductSession(store: FigureProductStore) {
|
|
|
57
106
|
error = undefined
|
|
58
107
|
saving = false
|
|
59
108
|
creationId = undefined
|
|
109
|
+
done = []
|
|
110
|
+
undone = []
|
|
60
111
|
}
|
|
61
112
|
function canLeave(discard: boolean) {
|
|
62
113
|
if (saving) throw Error('저장이 끝난 뒤 모델을 전환해 주세요.')
|
|
@@ -64,6 +115,11 @@ export function createFigureProductSession(store: FigureProductStore) {
|
|
|
64
115
|
}
|
|
65
116
|
return {
|
|
66
117
|
read,
|
|
118
|
+
/**
|
|
119
|
+
* The source the server is confirmed to hold: loaded, or answered by a save. What a revert sends as `expectedSource`
|
|
120
|
+
* -- the server writes over it only while it still holds this. Nothing for a figure not saved yet.
|
|
121
|
+
*/
|
|
122
|
+
confirmedSource: () => (id ? baseline : undefined),
|
|
67
123
|
startNew({ discard = false } = {}) {
|
|
68
124
|
canLeave(discard)
|
|
69
125
|
++epoch
|
|
@@ -104,45 +160,79 @@ export function createFigureProductSession(store: FigureProductStore) {
|
|
|
104
160
|
// AI must supply the context AND revision captured when it started. Never substitute current values.
|
|
105
161
|
if (origin === 'ai' && (context === undefined || expectedRevision === undefined))
|
|
106
162
|
throw Error('AI 편집에는 요청 당시의 모델 문맥과 리비전이 필요합니다.')
|
|
107
|
-
return
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
163
|
+
return tracked('do', () =>
|
|
164
|
+
workspace.apply({
|
|
165
|
+
...control,
|
|
166
|
+
origin,
|
|
167
|
+
context: context ?? control.context,
|
|
168
|
+
expectedRevision: expectedRevision ?? control.expectedRevision,
|
|
169
|
+
changes
|
|
170
|
+
})
|
|
171
|
+
)
|
|
114
172
|
},
|
|
115
173
|
/**
|
|
116
174
|
* An AI proposal (ADR-0094): the modeller's commands, as one step of the history, on the version the model saw.
|
|
117
175
|
* Never on the current one instead -- a person's edit in between makes the proposal stale, and it is refused.
|
|
118
176
|
*/
|
|
119
|
-
applyProposal(steps: readonly FigureProposalStep[], expectedRevision: number, context: string) {
|
|
177
|
+
applyProposal(steps: readonly FigureProposalStep[], expectedRevision: number, context: string, key?: string) {
|
|
120
178
|
writable()
|
|
121
|
-
|
|
179
|
+
/* Never with consent: only the person may let a clip go, and an AI proposal is not the person (designer 2026-09-27). */
|
|
180
|
+
return tracked('do', () => workspace.applyProposal({ origin: 'ai', context, expectedRevision, steps }), key)
|
|
181
|
+
},
|
|
182
|
+
/**
|
|
183
|
+
* A new figure made from an AI proposal is created under the proposal's key (ADR-0097 §5.6 item 5): the same
|
|
184
|
+
* proposal taken twice meets the first figure on the server, never a second one. Only on a new figure not yet
|
|
185
|
+
* touched -- one being made is the person's, and keeps its own.
|
|
186
|
+
*/
|
|
187
|
+
createUnder(key: string) {
|
|
188
|
+
if (id || workspace.read().dirty) throw Error('손대지 않은 새 모델에만 쓸 수 있습니다.')
|
|
189
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(key)) throw Error('제안 식별자가 올바르지 않습니다.')
|
|
190
|
+
creationId = key
|
|
122
191
|
},
|
|
192
|
+
/** The AI proposals whose steps are in the model now, each once. */
|
|
193
|
+
proposalsInEffect: inEffect,
|
|
123
194
|
/**
|
|
124
195
|
* A recipe put in by the person (ADR-0095): its resolved commands, as one step of the history, on the version open
|
|
125
196
|
* now. The same path as a proposal; the person is its origin.
|
|
126
197
|
*/
|
|
127
198
|
applyRecipe(steps: readonly FigureProposalStep[]) {
|
|
128
199
|
const at = writable()
|
|
129
|
-
return workspace.applyProposal({ origin: 'ui', context: at.context, expectedRevision: at.expectedRevision, steps })
|
|
200
|
+
return tracked('do', () => workspace.applyProposal({ origin: 'ui', context: at.context, expectedRevision: at.expectedRevision, steps, ...consented() }))
|
|
130
201
|
},
|
|
131
202
|
add(part: FigurePrimitiveCreation) {
|
|
132
|
-
return workspace.addPrimitive({ ...writable(), part })
|
|
203
|
+
return tracked('do', () => workspace.addPrimitive({ ...writable(), part }))
|
|
133
204
|
},
|
|
134
205
|
edit(edit: FigurePartEdit) {
|
|
135
|
-
return workspace.editPart({ ...writable(), edit })
|
|
206
|
+
return tracked('do', () => workspace.editPart({ ...writable(), edit, ...consented() }))
|
|
207
|
+
},
|
|
208
|
+
/** What a command would do, on a copy (ADR-0097 §5.5): nothing is written, and the answer names its version. */
|
|
209
|
+
previewCommand(action: FigureAuthoringAction) {
|
|
210
|
+
const view = workspace.read()
|
|
211
|
+
if (loading || error || !view.context || view.revision === null) throw Error('먼저 모델을 정상적으로 열어 주세요.')
|
|
212
|
+
return workspace.previewCommand({ context: view.context, action })
|
|
136
213
|
},
|
|
137
214
|
/** One authoring command — what a person means, not a graph edit — as one step of the same history. */
|
|
138
215
|
command(action: FigureAuthoringAction) {
|
|
139
|
-
return workspace.command({ ...writable(), action })
|
|
216
|
+
return tracked('do', () => workspace.command({ ...writable(), action, ...consented() }))
|
|
217
|
+
},
|
|
218
|
+
/**
|
|
219
|
+
* The first edit made inside `run` may let go of exactly these clips -- the ones a CLIPS_DROPPED refusal named and
|
|
220
|
+
* the person agreed to -- on the figure and version they were named on (designer 2026-09-27). Any other edit that
|
|
221
|
+
* would stop a clip is refused and asked about.
|
|
222
|
+
*/
|
|
223
|
+
withConsent<T>(given: FigureClipConsentFor, run: () => T): T {
|
|
224
|
+
consent = given
|
|
225
|
+
try {
|
|
226
|
+
return run()
|
|
227
|
+
} finally {
|
|
228
|
+
consent = undefined
|
|
229
|
+
}
|
|
140
230
|
},
|
|
141
231
|
undo() {
|
|
142
|
-
return workspace.undo(writable())
|
|
232
|
+
return tracked('undo', () => workspace.undo(writable()))
|
|
143
233
|
},
|
|
144
234
|
redo() {
|
|
145
|
-
return workspace.redo(writable())
|
|
235
|
+
return tracked('redo', () => workspace.redo(writable()))
|
|
146
236
|
},
|
|
147
237
|
source() {
|
|
148
238
|
const { context } = writable()
|
|
@@ -159,16 +249,62 @@ export function createFigureProductSession(store: FigureProductStore) {
|
|
|
159
249
|
const control = writable(),
|
|
160
250
|
requestEpoch = epoch
|
|
161
251
|
const exported = workspace.export(control)
|
|
252
|
+
/* Read with the text it goes with: an edit while saving is not what this save holds. */
|
|
253
|
+
const proposalKeys = inEffect()
|
|
162
254
|
saving = true
|
|
163
255
|
try {
|
|
164
|
-
|
|
256
|
+
let record: FigureProductRecord
|
|
257
|
+
try {
|
|
258
|
+
record = await store.save({ id, source: exported.text, expectedSource: baseline, creationId, thumbnail, proposalKeys, ...(id || !type ? {} : { type }) })
|
|
259
|
+
} catch (error) {
|
|
260
|
+
/*
|
|
261
|
+
A save that failed may still have landed: the answer lost on the way back (SAVE_UNREACHABLE), or a second
|
|
262
|
+
try refused because the first did land -- its expected source is the old one (FIGURE_SOURCE_CONFLICT). Only
|
|
263
|
+
those are read back; a refusal on grounds -- permission, validation -- stays a failure whatever the row
|
|
264
|
+
holds. Read back, every field the save writes must be what was sent: the source, the picture, the type
|
|
265
|
+
(designer 2026-09-27: a network failure alone does not say nothing was saved; nor does a matching source
|
|
266
|
+
alone say it was).
|
|
267
|
+
*/
|
|
268
|
+
const code = (error as { code?: string })?.code
|
|
269
|
+
const unknown = code === 'SAVE_UNREACHABLE' || code === 'FIGURE_SOURCE_CONFLICT'
|
|
270
|
+
const landed = unknown && id !== undefined && requestEpoch === epoch && store.readBack ? await store.readBack(id).catch(() => undefined) : undefined
|
|
271
|
+
const held = landed ? serializeFigureAsset(parseFigureAsset(landed.source)) : undefined
|
|
272
|
+
const same = !!landed && held === exported.text && (thumbnail === undefined || landed.thumbnail === thumbnail) && (type === undefined || landed.type === type)
|
|
273
|
+
if (same) record = landed!
|
|
274
|
+
else if (
|
|
275
|
+
code === 'FIGURE_SOURCE_CONFLICT' &&
|
|
276
|
+
landed &&
|
|
277
|
+
held !== undefined &&
|
|
278
|
+
unconfirmed.has(held) &&
|
|
279
|
+
/* Every field that earlier save wrote, not the source alone: nothing not compared is written over. */
|
|
280
|
+
(unconfirmed.get(held)!.thumbnail === undefined || landed.thumbnail === unconfirmed.get(held)!.thumbnail)
|
|
281
|
+
) {
|
|
282
|
+
/*
|
|
283
|
+
The row holds a text this session sent and never heard back about -- edited further since, the new text
|
|
284
|
+
is not it. The server is known to hold what was sent: this save is sent again, once, conditional on that
|
|
285
|
+
server text as the expected source (designer 2026-09-27: "the sent content is confirmed on the server").
|
|
286
|
+
*/
|
|
287
|
+
baseline = landed.source
|
|
288
|
+
unconfirmed.clear()
|
|
289
|
+
record = await store.save({ id, source: exported.text, expectedSource: baseline, creationId, thumbnail, proposalKeys, ...(id || !type ? {} : { type }) })
|
|
290
|
+
} else {
|
|
291
|
+
if (code === 'SAVE_UNREACHABLE') unconfirmed.set(exported.text, { thumbnail })
|
|
292
|
+
throw error
|
|
293
|
+
}
|
|
294
|
+
}
|
|
165
295
|
if (requestEpoch !== epoch) return read()
|
|
166
296
|
if (!record.id || (id !== undefined && record.id !== id)) throw Error('저장 응답의 Figure 대상이 다릅니다.')
|
|
167
297
|
if (serializeFigureAsset(parseFigureAsset(record.source)) !== exported.text)
|
|
168
298
|
throw Error('저장 응답이 보낸 모델과 다릅니다. 저장 상태를 다시 확인해 주세요.')
|
|
169
|
-
|
|
299
|
+
/* The server has it: the next save expects it, whatever happened on screen meanwhile. */
|
|
170
300
|
id = record.id
|
|
171
301
|
baseline = record.source
|
|
302
|
+
unconfirmed.clear()
|
|
303
|
+
/*
|
|
304
|
+
Edited while it was saving: what was sent is saved, the edits since are not -- they stay, and the model stays
|
|
305
|
+
unsaved (the workspace refuses a stale acknowledgement). Never marked saved, never wiped.
|
|
306
|
+
*/
|
|
307
|
+
if (workspace.read().revision === exported.revision) workspace.acknowledgeSaved(exported)
|
|
172
308
|
} finally {
|
|
173
309
|
if (requestEpoch === epoch) saving = false
|
|
174
310
|
}
|
|
@@ -140,3 +140,18 @@ export function inputRangeRefusalOf(error: unknown, say: Say): string | null {
|
|
|
140
140
|
const params = { value: number(value!), min: number(min!), max: number(max!) }
|
|
141
141
|
return axis ? say('figure.text.input-range-size', { ...params, axis: axis.toUpperCase() }) : say('figure.text.input-range', params)
|
|
142
142
|
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Why "follow the size and keep where the figure is placed" was not applied, in the author's words (ADR-0097): the plan
|
|
146
|
+
* says what it would have had to overwrite, and for a part through the plane how far. A code the screen has no words
|
|
147
|
+
* for is said as a refusal with its code, never in the kernel's English.
|
|
148
|
+
*/
|
|
149
|
+
export function resizeRefusalOf(plan: { mode: string; refusal?: { code: string; axis?: string; mm?: number } }, part: string, say: Say): string {
|
|
150
|
+
const refusal = plan.refusal
|
|
151
|
+
if (!refusal) return ''
|
|
152
|
+
const place = say(`figure.label.resize-place-${plan.mode}`, {})
|
|
153
|
+
const params = { part, place, axis: (refusal.axis ?? '').toUpperCase(), mm: refusal.mm === undefined ? '' : String(Math.round(refusal.mm)) }
|
|
154
|
+
const known = ['HELD', 'TURNED', 'UNIFORM_POLICY', 'SHAPE_EDIT', 'DIMENSION_RULE', 'POSITION_RULE', 'GAP_CHOICE', 'NO_FACE']
|
|
155
|
+
if (known.includes(refusal.code)) return say(`figure.text.resize-refused-${refusal.code.toLowerCase().replace(/_/g, '-')}`, params)
|
|
156
|
+
return say('figure.text.edit-refused', { code: refusal.code })
|
|
157
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The request id a revert or an undo goes with (ADR-0097 §5.6, designer 2026-09-27: a retry after a lost answer never
|
|
3
|
+
* runs the same restore twice).
|
|
4
|
+
*
|
|
5
|
+
* Asked again for the same thing -- the same version over the same confirmed source, or the same kept draft -- after
|
|
6
|
+
* an answer that never came, the id is the one already sent: the server answers with what it did. Anything else is a
|
|
7
|
+
* new request.
|
|
8
|
+
*/
|
|
9
|
+
export interface RevertRequest {
|
|
10
|
+
key: string
|
|
11
|
+
requestId: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Outcomes that leave it unknown whether the server did it. */
|
|
15
|
+
const UNKNOWN = new Set(['REVERT_UNREACHABLE', 'FIGURE_REVERT_IN_FLIGHT'])
|
|
16
|
+
|
|
17
|
+
export function revertRequestFor(pending: RevertRequest | undefined, key: string, fresh: () => string = () => crypto.randomUUID()): RevertRequest {
|
|
18
|
+
return pending?.key === key ? pending : { key, requestId: fresh() }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** After the answer, or its absence: kept only while it is not known whether the server did it. */
|
|
22
|
+
export function afterRevertAnswer(sent: RevertRequest, code?: string): RevertRequest | undefined {
|
|
23
|
+
return code !== undefined && UNKNOWN.has(code) ? sent : undefined
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const revertOutcomeUnknown = (code?: string) => code !== undefined && UNKNOWN.has(code)
|
|
@@ -263,6 +263,18 @@ export const SidePanelStyles = css`
|
|
|
263
263
|
div[cell] input:focus {
|
|
264
264
|
outline: none;
|
|
265
265
|
}
|
|
266
|
+
/* An axis a rule sets: its number, read-only, on a quieter ground (ADR-0097). */
|
|
267
|
+
div[cell][held] {
|
|
268
|
+
background-color: var(--md-sys-color-surface-container);
|
|
269
|
+
border-style: dashed;
|
|
270
|
+
}
|
|
271
|
+
div[cell][held] input {
|
|
272
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
273
|
+
font-style: italic;
|
|
274
|
+
}
|
|
275
|
+
p[held-axes] {
|
|
276
|
+
margin: 2px 0 6px;
|
|
277
|
+
}
|
|
266
278
|
label[toggle] {
|
|
267
279
|
display: flex;
|
|
268
280
|
align-items: center;
|
|
@@ -64,8 +64,16 @@ function colours(part: FigureRenderPart, palette: Record<string, string>) {
|
|
|
64
64
|
return { color: palette[m.token] ?? '#9a9a9a', preset: m.preset, transparent: !!m.transparent, ...(glow ? { glow } : {}) }
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
/**
|
|
68
|
-
export
|
|
67
|
+
/** Per axis of the body's own frame, the face a size drag keeps: 1 the low face, -1 the high face, 0 the centre. */
|
|
68
|
+
export type ScaleAnchor = Record<'x' | 'y' | 'z', -1 | 0 | 1>
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The component state that draws a body where figure-model put it. With `anchor` -- figure-model's
|
|
72
|
+
* `figureScaleAnchorOf` for its part -- the scene's scale gizmo keeps that face while it is dragged, the face the model
|
|
73
|
+
* keeps when the drag is written; without it the gizmo keeps the face across from the handle held, and the part jumped
|
|
74
|
+
* on release (designer 2026-09-27). Body x · y · z are the component's width · depth · height, as the scene's axes are.
|
|
75
|
+
*/
|
|
76
|
+
export function componentOf(part: FigureRenderPart, palette: Record<string, string>, board: Board, anchor?: ScaleAnchor): Record<string, unknown> | null {
|
|
69
77
|
const frame = frameOf(part)
|
|
70
78
|
if (!frame) return null
|
|
71
79
|
const { centre, extent, rotation, origin } = frame
|
|
@@ -88,7 +96,8 @@ export function componentOf(part: FigureRenderPart, palette: Record<string, stri
|
|
|
88
96
|
zPos: round(world.y - depth / 2),
|
|
89
97
|
rotationX: round(euler.x),
|
|
90
98
|
rotation: round(-euler.y),
|
|
91
|
-
rotationY: round(euler.z)
|
|
99
|
+
rotationY: round(euler.z),
|
|
100
|
+
...(anchor ? { scaleAnchor: { ...anchor } } : {})
|
|
92
101
|
}
|
|
93
102
|
}
|
|
94
103
|
|
|
@@ -181,3 +190,16 @@ export function boardFor(parts: readonly FigureRenderPart[], at: Board & { depth
|
|
|
181
190
|
depth: Math.max(at.depth ?? 0, Math.ceil(size.y * 1.1), 200)
|
|
182
191
|
}
|
|
183
192
|
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* A body's placement as its fields would read it: its origin in mm and its turn in degrees (rigid@1's X·Y·Z order, as
|
|
196
|
+
* `componentOf` reads it). For a field a rule sets -- a fastened part's height, a turn its joint fixes -- the screen shows
|
|
197
|
+
* this number read-only rather than a blank (designer 2026-09-27).
|
|
198
|
+
*/
|
|
199
|
+
export function placementOf(part: FigureRenderPart): { position: [number, number, number]; rotation: [number, number, number] } {
|
|
200
|
+
const r = part.pose.r
|
|
201
|
+
const m = new THREE.Matrix4().set(r[0]![0]!, r[0]![1]!, r[0]![2]!, 0, r[1]![0]!, r[1]![1]!, r[1]![2]!, 0, r[2]![0]!, r[2]![1]!, r[2]![2]!, 0, 0, 0, 0, 1)
|
|
202
|
+
const e = new THREE.Euler().setFromRotationMatrix(m, 'XYZ')
|
|
203
|
+
const deg = (v: number) => Math.round(THREE.MathUtils.radToDeg(v) * 10) / 10 || 0
|
|
204
|
+
return { position: part.pose.t.map(round) as [number, number, number], rotation: [deg(e.x), deg(e.y), deg(e.z)] }
|
|
205
|
+
}
|
|
@@ -6,7 +6,7 @@ import { modellerPathOf } from '../modeller/modeller-route.js'
|
|
|
6
6
|
import { createFigureProductSession } from '../modeller/product-session.js'
|
|
7
7
|
import { figureProductStore } from '../graphql/store.js'
|
|
8
8
|
import { i18next } from '@operato/i18n'
|
|
9
|
-
import { fetchFigure, fetchFigureTypeNames, updateFigure } from '../graphql/index.js'
|
|
9
|
+
import { fetchFigure, fetchFigureTypeNames, fetchProposalOutcome, updateFigure } from '../graphql/index.js'
|
|
10
10
|
import { availableRecipes, recipeGuide } from '../modeller/recipe-source.js'
|
|
11
11
|
import type { RecipeTemplate } from '../modeller/recipes.js'
|
|
12
12
|
import { occupancyFromParts } from '../modeller/occupancy.js'
|
|
@@ -18,17 +18,18 @@ import '../modeller/figure-verdict.js'
|
|
|
18
18
|
import '../modeller/figure-properties.js'
|
|
19
19
|
import type { FigureBudgetLine } from '../modeller/maturity.js'
|
|
20
20
|
import { OxPrompt } from '@operato/popup'
|
|
21
|
-
import { openPopup } from '@operato/layout'
|
|
21
|
+
import { notify, openPopup } from '@operato/layout'
|
|
22
22
|
import '../modeller/figure-first-save.js'
|
|
23
23
|
import { activePalette } from '@hatiolab/things-scene'
|
|
24
24
|
import {
|
|
25
|
+
askAssistant,
|
|
25
26
|
clearAssistantSurface,
|
|
26
27
|
notifyAssistantSurface,
|
|
27
28
|
provideAssistantSurface
|
|
28
29
|
} from '@things-factory/ai-assistant/dist-client/index.js'
|
|
29
30
|
import type { FirstSave } from '../modeller/first-save.js'
|
|
30
31
|
import { placementRefusalOf } from '../modeller/refusals.js'
|
|
31
|
-
import { takePendingNewFigure } from '../modeller/ai-proposal.js'
|
|
32
|
+
import { proposalOutcomeFrom, takeFigureOffer, takePendingNewFigure } from '../modeller/ai-proposal.js'
|
|
32
33
|
|
|
33
34
|
/**
|
|
34
35
|
* The product page has exactly one authoritative workspace for UI and AI input transactions.
|
|
@@ -161,6 +162,9 @@ export class FigureModellerPage extends PageView {
|
|
|
161
162
|
label: () => this.session.read().name || id,
|
|
162
163
|
detail: () => (this.session.read().dirty ? i18next.t('figure.text.unsaved-changes') : ''),
|
|
163
164
|
anchor: () => ({ anchorType: 'figure', anchorId: id }),
|
|
165
|
+
/* The card's "check the result": the server's record, never the dock's memory; nothing is applied again. */
|
|
166
|
+
proposalOutcome: async (proposalKey: string) =>
|
|
167
|
+
proposalOutcomeFrom(await fetchProposalOutcome(proposalKey), (key, params) => i18next.t(key, params)),
|
|
164
168
|
hostContext: () => ({
|
|
165
169
|
surface: 'figure-modeller',
|
|
166
170
|
editorMode: 'existing',
|
|
@@ -169,10 +173,23 @@ export class FigureModellerPage extends PageView {
|
|
|
169
173
|
/* The version the model sees; a proposal is applied on it, or refused if the figure moved on (ADR-0094). */
|
|
170
174
|
context: this.session.read().context,
|
|
171
175
|
revision: this.session.read().revision,
|
|
172
|
-
palette: Object.keys(activePalette())
|
|
176
|
+
palette: Object.keys(activePalette()),
|
|
177
|
+
...this.assistantFocus()
|
|
173
178
|
})
|
|
174
179
|
})
|
|
175
180
|
}
|
|
181
|
+
/**
|
|
182
|
+
* The part selected and what the preview is trying, with the version they were seen on (ADR-0097 §5.6 item 5). Read
|
|
183
|
+
* when a request is sent; selecting is not a new subject, so the dock is not told about it and the conversation stays.
|
|
184
|
+
*/
|
|
185
|
+
private assistantFocus() {
|
|
186
|
+
const focus = this.renderRoot.querySelector<HTMLElement & { assistantFocus?: () => { part?: string; trying?: object } }>('figure-editor')?.assistantFocus?.() ?? {}
|
|
187
|
+
const { context, revision } = this.session.read()
|
|
188
|
+
return {
|
|
189
|
+
...(focus.part ? { selection: { part: focus.part, context, revision } } : {}),
|
|
190
|
+
...(focus.trying ? { trying: { ...focus.trying, context, revision } } : {})
|
|
191
|
+
}
|
|
192
|
+
}
|
|
176
193
|
private withdrawAssistant() {
|
|
177
194
|
this.assistantFor = undefined
|
|
178
195
|
clearAssistantSurface(this)
|
|
@@ -201,9 +218,19 @@ export class FigureModellerPage extends PageView {
|
|
|
201
218
|
if (this.wasDirty && !view.dirty && !view.saving && view.id) this.readRow(view.id)
|
|
202
219
|
this.wasDirty = !!view.dirty
|
|
203
220
|
}
|
|
221
|
+
/*
|
|
222
|
+
A released figure changed on the server -- its source saved, or its category or tags -- comes back a draft, and a
|
|
223
|
+
board keeps the released version until it is released again. Said when it happens (D, designer 2026-09-27), not left
|
|
224
|
+
to a badge in the history tab.
|
|
225
|
+
*/
|
|
226
|
+
/* True of what boards read: `figureBlueprint` answers a draft row with its latest released version (releasedSourceOf). */
|
|
227
|
+
private toldDemoted(before: string | undefined, after: string | undefined) {
|
|
228
|
+
if (before === 'released' && after === 'draft') notify({ level: 'info', message: i18next.t('figure.text.released-now-draft') })
|
|
229
|
+
}
|
|
204
230
|
private async readRow(id: string) {
|
|
205
231
|
try {
|
|
206
232
|
const figure = await fetchFigure(id)
|
|
233
|
+
if (this.session.read().id === id) this.toldDemoted(this.row.state, figure.state)
|
|
207
234
|
if (this.session.read().id === id)
|
|
208
235
|
this.row = {
|
|
209
236
|
state: figure.state as 'draft' | 'released',
|
|
@@ -290,7 +317,15 @@ export class FigureModellerPage extends PageView {
|
|
|
290
317
|
try {
|
|
291
318
|
const result = await updateFigure(id, patch)
|
|
292
319
|
this.commandError = ''
|
|
293
|
-
|
|
320
|
+
/* The server's answer on its state too: a category changed on a released figure made it a draft, and the badge stayed. */
|
|
321
|
+
this.toldDemoted(this.row.state, result.figure.state)
|
|
322
|
+
this.row = {
|
|
323
|
+
...this.row,
|
|
324
|
+
category: result.figure.category ?? undefined,
|
|
325
|
+
tags: result.figure.tags ?? undefined,
|
|
326
|
+
...(result.figure.state ? { state: result.figure.state as 'draft' | 'released' } : {}),
|
|
327
|
+
...(result.figure.version !== undefined ? { version: result.figure.version ?? 0 } : {})
|
|
328
|
+
}
|
|
294
329
|
} catch (error) {
|
|
295
330
|
this.commandError = error instanceof Error ? error.message : String(error)
|
|
296
331
|
}
|
|
@@ -300,7 +335,22 @@ export class FigureModellerPage extends PageView {
|
|
|
300
335
|
/* Keep the rest of the row: rebuilding it from these two dropped the type, and the locked type field went blank. */
|
|
301
336
|
if (figure?.id && figure.id === this.session.read().id) this.row = { ...this.row, state: figure.state, version: figure.version ?? 0 }
|
|
302
337
|
}
|
|
303
|
-
|
|
338
|
+
/*
|
|
339
|
+
The edits in hand saved before going back to a released version: known saved, or not -- a failure, or an outcome not
|
|
340
|
+
known, holds the revert (designer 2026-09-27, Q-D1). The figure has a row here: there is no first save to ask.
|
|
341
|
+
*/
|
|
342
|
+
private saveBeforeRevert = async (): Promise<{ ok: boolean; reason?: string }> => {
|
|
343
|
+
try {
|
|
344
|
+
await this.session.save()
|
|
345
|
+
this.changed()
|
|
346
|
+
return this.session.read().dirty ? { ok: false } : { ok: true }
|
|
347
|
+
} catch (error) {
|
|
348
|
+
this.changed()
|
|
349
|
+
const code = (error as { code?: string })?.code
|
|
350
|
+
return { ok: false, reason: code === 'SAVE_UNREACHABLE' ? i18next.t('figure.text.save-unreachable') : error instanceof Error ? error.message : String(error) }
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
/** 과거 판으로 되돌렸으면 서버의 원본이 바뀐 것이다 — 다시 연다. 저장 안 된 것이 있으면 패널이 먼저 저장·버리기·취소를 묻는다. */
|
|
304
354
|
private reverted = async () => {
|
|
305
355
|
const id = this.session.read().id
|
|
306
356
|
if (!id) return
|
|
@@ -316,26 +366,7 @@ export class FigureModellerPage extends PageView {
|
|
|
316
366
|
private applyAI = (event: Event) => {
|
|
317
367
|
const detail = (event as CustomEvent).detail
|
|
318
368
|
if (!this.active || !detail) return
|
|
319
|
-
|
|
320
|
-
if (detail.create && this.routeId) return
|
|
321
|
-
if (detail.create && view.dirty) {
|
|
322
|
-
/* A new figure already being made is the person's; a proposal does not go on top of it or replace it. */
|
|
323
|
-
detail.applied = false
|
|
324
|
-
detail.code = 'NEW_FIGURE_UNSAVED'
|
|
325
|
-
detail.error = i18next.t('figure.text.proposal-new-figure-unsaved')
|
|
326
|
-
return
|
|
327
|
-
}
|
|
328
|
-
try {
|
|
329
|
-
if (detail.create) this.session.applyProposal(detail.steps, view.revision!, view.context!)
|
|
330
|
-
else if (detail.steps) this.session.applyProposal(detail.steps, detail.revision, detail.context)
|
|
331
|
-
else this.session.apply(detail.changes, 'ai', detail.revision, detail.context)
|
|
332
|
-
detail.applied = true
|
|
333
|
-
this.changed()
|
|
334
|
-
} catch (error) {
|
|
335
|
-
detail.applied = false
|
|
336
|
-
detail.code = (error as { code?: string })?.code
|
|
337
|
-
detail.error = error instanceof Error ? error.message : String(error)
|
|
338
|
-
}
|
|
369
|
+
if (takeFigureOffer(this.session, detail, { onSavedFigure: !!this.routeId, say: (key, params) => i18next.t(key, params) })) this.changed()
|
|
339
370
|
}
|
|
340
371
|
/*
|
|
341
372
|
Reloading or closing the tab is not a move the shell asks `canDeactivate` about: a new figure not yet saved was lost
|
|
@@ -420,7 +451,9 @@ export class FigureModellerPage extends PageView {
|
|
|
420
451
|
if (!waiting) return
|
|
421
452
|
const view = this.session.read()
|
|
422
453
|
try {
|
|
423
|
-
|
|
454
|
+
/* Created under the proposal's key: taken twice, it meets the first figure, never a second (ADR-0097 §5.6 item 5). */
|
|
455
|
+
if (waiting.key) this.session.createUnder(waiting.key)
|
|
456
|
+
this.session.applyProposal(waiting.steps, view.revision!, view.context!, waiting.key)
|
|
424
457
|
waiting.settle({ applied: true })
|
|
425
458
|
} catch (error) {
|
|
426
459
|
waiting.settle({ applied: false, code: (error as { code?: string })?.code, error: error instanceof Error ? error.message : String(error) })
|
|
@@ -429,11 +462,14 @@ export class FigureModellerPage extends PageView {
|
|
|
429
462
|
/** The first save's question, over the names this domain already uses. */
|
|
430
463
|
private askFirstSave = async (name: string): Promise<FirstSave | undefined> => {
|
|
431
464
|
const taken = await fetchFigureTypeNames().catch(() => [] as string[])
|
|
432
|
-
|
|
465
|
+
const answer = await openPopup<FirstSave>(html`<figure-first-save .type=${this.row.type ?? ''} .name=${name} .taken=${taken}></figure-first-save>`, {
|
|
433
466
|
backdrop: true,
|
|
434
467
|
size: 'small',
|
|
435
468
|
title: i18next.t('figure.title.save-new-figure')
|
|
436
469
|
}).result
|
|
470
|
+
/* Kept: a first save that failed opens again on the same identifier, not a new draw (ADR-0097 §5.6, Q-D3). */
|
|
471
|
+
if (answer) this.row = { ...this.row, type: answer.type }
|
|
472
|
+
return answer
|
|
437
473
|
}
|
|
438
474
|
get context() {
|
|
439
475
|
return {
|
|
@@ -450,7 +486,8 @@ export class FigureModellerPage extends PageView {
|
|
|
450
486
|
render() {
|
|
451
487
|
if (this.error) return html`<p role="alert" class="error">${this.error}</p>`
|
|
452
488
|
const view = this.session.read()
|
|
453
|
-
|
|
489
|
+
/* A new figure's third way to start: the app's assistant dock, with a request put in its input for the author to finish and send (B, U04). */
|
|
490
|
+
return html`<figure-editor .session=${this.session} .releaseState=${this.row} .templates=${this.templates} .guideOf=${this.guideOf} .thumbnailOf=${this.thumbnailOf} .firstSave=${this.askFirstSave} .revision=${this.revision} .instances=${this.instances} .budget=${this.budget} aiDraft @ask-ai-draft=${() => askAssistant({ draft: i18next.t('figure.text.ai-draft-request') })} @changed=${this.changed}>
|
|
454
491
|
${this.commandError ? html`<p slot="notice" fit-error role="alert">${this.commandError}</p>` : ''}
|
|
455
492
|
<figure-properties
|
|
456
493
|
slot="figure"
|
|
@@ -491,6 +528,8 @@ export class FigureModellerPage extends PageView {
|
|
|
491
528
|
.figureVersion=${this.row.version}
|
|
492
529
|
.figureType=${this.row.type}
|
|
493
530
|
.dirty=${!!view.dirty}
|
|
531
|
+
.saveFirst=${this.saveBeforeRevert}
|
|
532
|
+
.serverSource=${() => this.session.confirmedSource()}
|
|
494
533
|
@fit-base-to-parts=${this.fitToParts}
|
|
495
534
|
@figure-released=${this.released}
|
|
496
535
|
@figure-reverted=${this.reverted}
|
|
@@ -18,8 +18,8 @@ registerProposalHandler(FIGURE_CANDIDATE_TOOL, async (proposal, context) => {
|
|
|
18
18
|
window.dispatchEvent(new CustomEvent('figure-apply', { detail }));
|
|
19
19
|
return detail;
|
|
20
20
|
},
|
|
21
|
-
openNew(steps) {
|
|
22
|
-
const opened = pendNewFigure(steps);
|
|
21
|
+
openNew(steps, key) {
|
|
22
|
+
const opened = pendNewFigure(steps, key);
|
|
23
23
|
navigate(modellerPathOf());
|
|
24
24
|
return opened;
|
|
25
25
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"figure-assistant.js","sourceRoot":"","sources":["../client/figure-assistant.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AACvC,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AACzC,OAAO,EAAE,uBAAuB,EAAE,MAAM,mDAAmD,CAAA;AAE3F,OAAO,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAA;AAC7D,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAA0B,MAAM,2BAA2B,CAAA;AAEpG,MAAM,CAAC,MAAM,qBAAqB,GAAG,wBAAwB,CAAA;AAE7D,uBAAuB,CAAC,qBAAqB,EAAE,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE;IACzE,MAAM,OAAO,GAAG,MAAM,iBAAiB,CACrC,QAAQ,EACR;QACE,KAAK,CAAC,MAAyB;YAC7B,MAAM,CAAC,aAAa,CAAC,IAAI,WAAW,CAAC,cAAc,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC,CAAA;YACjE,OAAO,MAAM,CAAA;QACf,CAAC;QACD,OAAO,CAAC,KAAK;
|
|
1
|
+
{"version":3,"file":"figure-assistant.js","sourceRoot":"","sources":["../client/figure-assistant.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AACvC,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AACzC,OAAO,EAAE,uBAAuB,EAAE,MAAM,mDAAmD,CAAA;AAE3F,OAAO,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAA;AAC7D,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAA0B,MAAM,2BAA2B,CAAA;AAEpG,MAAM,CAAC,MAAM,qBAAqB,GAAG,wBAAwB,CAAA;AAE7D,uBAAuB,CAAC,qBAAqB,EAAE,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE;IACzE,MAAM,OAAO,GAAG,MAAM,iBAAiB,CACrC,QAAQ,EACR;QACE,KAAK,CAAC,MAAyB;YAC7B,MAAM,CAAC,aAAa,CAAC,IAAI,WAAW,CAAC,cAAc,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC,CAAA;YACjE,OAAO,MAAM,CAAA;QACf,CAAC;QACD,OAAO,CAAC,KAAK,EAAE,GAAG;YAChB,MAAM,MAAM,GAAG,aAAa,CAAC,KAAK,EAAE,GAAG,CAAC,CAAA;YACxC,QAAQ,CAAC,cAAc,EAAE,CAAC,CAAA;YAC1B,OAAO,MAAM,CAAA;QACf,CAAC;KACF,EACD,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,EAAE,MAAM,CAAC,CACxC,CAAA;IACD,IAAI,OAAO,CAAC,IAAI;QAAE,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,OAAO,KAAK,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,CAAA;IACjG,OAAO,OAAO,CAAA;AAChB,CAAC,CAAC,CAAA","sourcesContent":["/*\n * What the AI dock does with a figure proposal - figure-ui declares it because figure-service declares the tool\n * (`proposeFigureCandidate`, design/ai/shared-dock.md §4).\n *\n * A proposal is a list of the modeller's own commands (ADR-0094). Running it applies them in the modeller as one step\n * of its history: a revision on the figure open there, at the version the model saw; a new figure on a new figure the\n * modeller opens. The outcome is said in words on every branch (`ai-proposal.ts`).\n */\nimport { i18next } from '@operato/i18n'\nimport { navigate } from '@operato/shell'\nimport { registerProposalHandler } from '@things-factory/ai-assistant/dist-client/index.js'\n\nimport { modellerPathOf } from './modeller/modeller-route.js'\nimport { pendNewFigure, runFigureProposal, type FigureApplyDetail } from './modeller/ai-proposal.js'\n\nexport const FIGURE_CANDIDATE_TOOL = 'proposeFigureCandidate'\n\nregisterProposalHandler(FIGURE_CANDIDATE_TOOL, async (proposal, context) => {\n const outcome = await runFigureProposal(\n proposal,\n {\n offer(detail: FigureApplyDetail) {\n window.dispatchEvent(new CustomEvent('figure-apply', { detail }))\n return detail\n },\n openNew(steps, key) {\n const opened = pendNewFigure(steps, key)\n navigate(modellerPathOf())\n return opened\n }\n },\n (key, params) => i18next.t(key, params)\n )\n if (outcome.note) context.notify(outcome.verdict === 'accepted' ? 'info' : 'error', outcome.note)\n return outcome\n})\n"]}
|
|
@@ -39,8 +39,45 @@ export declare function deleteFigure(id: string): Promise<boolean>;
|
|
|
39
39
|
* 발행을 푼다(초안으로 돌아온다).
|
|
40
40
|
*/
|
|
41
41
|
export declare function releaseFigure(id: string, comment?: string): Promise<Figure>;
|
|
42
|
-
/**
|
|
43
|
-
export
|
|
42
|
+
/** A draft a revert kept when it wrote over it (ADR-0097 §5.6). Not a released version. */
|
|
43
|
+
export interface KeptDraft {
|
|
44
|
+
id: string;
|
|
45
|
+
kind: 'revert' | 'undo';
|
|
46
|
+
state?: string;
|
|
47
|
+
version?: number;
|
|
48
|
+
revertedToVersion?: number;
|
|
49
|
+
restoredAt?: string;
|
|
50
|
+
createdAt?: string;
|
|
51
|
+
creator?: {
|
|
52
|
+
name?: string;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* 옛 판을 초안으로 되살린다. 정본·속성·그림이 돌아오고 이름·설명은 그대로다. 덮이는 초안은 같은 트랜잭션에서 서버가
|
|
57
|
+
* 보관한다. `expectedSource` 는 서버가 들고 있다고 확인된 정본 -- 그 사이 바뀌었으면 거절된다. 답을 못 받았으면
|
|
58
|
+
* (REVERT_UNREACHABLE) 같은 `requestId` 로 다시 묻는다: 서버는 같은 되돌리기로 답하고 두 번 하지 않는다.
|
|
59
|
+
*/
|
|
60
|
+
export declare function revertFigureVersion(id: string, version: number, requestId: string, expectedSource: string): Promise<SaveResult & {
|
|
61
|
+
kept: KeptDraft;
|
|
62
|
+
}>;
|
|
63
|
+
/** 되돌리기 취소 -- 보관한 초안을 되살린다. 되돌린 뒤 모델이 바뀌었으면 서버가 거절하고 보관본은 남는다. */
|
|
64
|
+
export declare function undoFigureRevert(keptId: string, requestId: string): Promise<SaveResult & {
|
|
65
|
+
kept: KeptDraft;
|
|
66
|
+
}>;
|
|
67
|
+
/** A figure an AI proposal was saved into (ADR-0097 §5.6 item 5). */
|
|
68
|
+
export interface ProposalOutcome {
|
|
69
|
+
figureId: string;
|
|
70
|
+
type?: string;
|
|
71
|
+
name?: string;
|
|
72
|
+
recordedAt?: string;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* 제안이 무엇이 됐나 -- 그 제안을 담은 저장이 들어간 도형들. 비었으면 저장되지 않았다(적용만 하고 저장 전이거나, 되돌렸거나,
|
|
76
|
+
* 적용하지 않았다). 도크의 「결과 확인」이 이것을 묻는다: 도크의 기억이 아니라 서버의 기록이다.
|
|
77
|
+
*/
|
|
78
|
+
export declare function fetchProposalOutcome(proposalKey: string): Promise<ProposalOutcome[]>;
|
|
79
|
+
/** 되돌리기가 보관한 초안들, 새것부터 -- 다시 접속해도 여기서 찾는다. */
|
|
80
|
+
export declare function fetchReplacedDrafts(id: string): Promise<KeptDraft[]>;
|
|
44
81
|
/**
|
|
45
82
|
* 발행해도 되나 — **누르기 전에** 묻는다.
|
|
46
83
|
*
|