@things-factory/figure-ui 10.1.168 → 10.1.170
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 +10 -1
- package/client/modeller/affected-parts.ts +11 -0
- package/client/modeller/ai-proposal.ts +40 -9
- package/client/modeller/canvas.ts +15 -1
- package/client/modeller/editor-styles.ts +98 -6
- package/client/modeller/editor.ts +212 -53
- package/client/modeller/fasten-geometry.ts +19 -0
- package/client/modeller/figure-fasten-drawing.ts +4 -3
- package/client/modeller/figure-links.ts +80 -37
- package/client/modeller/figure-material-panel.ts +20 -1
- package/client/modeller/figure-preview.ts +117 -8
- package/client/modeller/figure-properties.ts +1 -2
- package/client/modeller/figure-section-panel.ts +5 -0
- package/client/modeller/figure-verdict.ts +9 -2
- package/client/modeller/gate-findings.ts +12 -4
- package/client/modeller/group-menu.ts +57 -0
- package/client/modeller/links.ts +22 -1
- package/client/modeller/modeller-keys.ts +6 -0
- package/client/modeller/preview-size.ts +32 -0
- package/client/modeller/product-session.ts +23 -9
- package/client/modeller/refusals.ts +14 -0
- package/client/modeller/stage-model.ts +11 -0
- package/client/pages/figure-modeller-page.ts +15 -6
- package/dist-client/figure-assistant.d.ts +6 -0
- package/dist-client/figure-assistant.js +9 -1
- package/dist-client/figure-assistant.js.map +1 -1
- package/dist-client/modeller/affected-parts.d.ts +6 -0
- package/dist-client/modeller/affected-parts.js +12 -0
- package/dist-client/modeller/affected-parts.js.map +1 -0
- package/dist-client/modeller/ai-proposal.d.ts +15 -5
- package/dist-client/modeller/ai-proposal.js +39 -10
- package/dist-client/modeller/ai-proposal.js.map +1 -1
- package/dist-client/modeller/canvas.d.ts +3 -0
- package/dist-client/modeller/canvas.js +12 -1
- package/dist-client/modeller/canvas.js.map +1 -1
- package/dist-client/modeller/editor-styles.js +98 -6
- package/dist-client/modeller/editor-styles.js.map +1 -1
- package/dist-client/modeller/editor.d.ts +42 -1
- package/dist-client/modeller/editor.js +220 -51
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/fasten-geometry.d.ts +27 -0
- package/dist-client/modeller/fasten-geometry.js +14 -0
- package/dist-client/modeller/fasten-geometry.js.map +1 -1
- package/dist-client/modeller/figure-fasten-drawing.js +4 -3
- package/dist-client/modeller/figure-fasten-drawing.js.map +1 -1
- package/dist-client/modeller/figure-links.d.ts +3 -1
- package/dist-client/modeller/figure-links.js +84 -38
- package/dist-client/modeller/figure-links.js.map +1 -1
- package/dist-client/modeller/figure-material-panel.d.ts +8 -1
- package/dist-client/modeller/figure-material-panel.js +24 -1
- package/dist-client/modeller/figure-material-panel.js.map +1 -1
- package/dist-client/modeller/figure-preview.d.ts +12 -0
- package/dist-client/modeller/figure-preview.js +116 -5
- package/dist-client/modeller/figure-preview.js.map +1 -1
- package/dist-client/modeller/figure-properties.js +1 -2
- package/dist-client/modeller/figure-properties.js.map +1 -1
- package/dist-client/modeller/figure-section-panel.d.ts +1 -1
- package/dist-client/modeller/figure-section-panel.js +5 -0
- package/dist-client/modeller/figure-section-panel.js.map +1 -1
- package/dist-client/modeller/figure-verdict.d.ts +1 -0
- package/dist-client/modeller/figure-verdict.js +8 -2
- package/dist-client/modeller/figure-verdict.js.map +1 -1
- package/dist-client/modeller/gate-findings.d.ts +3 -1
- package/dist-client/modeller/gate-findings.js +8 -4
- package/dist-client/modeller/gate-findings.js.map +1 -1
- package/dist-client/modeller/group-menu.d.ts +18 -0
- package/dist-client/modeller/group-menu.js +52 -0
- package/dist-client/modeller/group-menu.js.map +1 -0
- package/dist-client/modeller/links.d.ts +5 -0
- package/dist-client/modeller/links.js +21 -1
- package/dist-client/modeller/links.js.map +1 -1
- package/dist-client/modeller/modeller-keys.js +7 -0
- package/dist-client/modeller/modeller-keys.js.map +1 -1
- package/dist-client/modeller/preview-size.d.ts +23 -0
- package/dist-client/modeller/preview-size.js +32 -0
- package/dist-client/modeller/preview-size.js.map +1 -0
- package/dist-client/modeller/product-session.d.ts +0 -1
- package/dist-client/modeller/product-session.js +22 -9
- package/dist-client/modeller/product-session.js.map +1 -1
- package/dist-client/modeller/refusals.d.ts +1 -5
- package/dist-client/modeller/refusals.js +14 -1
- package/dist-client/modeller/refusals.js.map +1 -1
- package/dist-client/modeller/stage-model.d.ts +11 -0
- package/dist-client/modeller/stage-model.js +11 -0
- package/dist-client/modeller/stage-model.js.map +1 -1
- package/dist-client/pages/figure-modeller-page.d.ts +3 -0
- package/dist-client/pages/figure-modeller-page.js +18 -5
- package/dist-client/pages/figure-modeller-page.js.map +1 -1
- package/dist-client/route.d.ts +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +8 -8
- package/test/ai-proposal.test.ts +43 -4
- package/test/browser/editing-page.js +3 -1
- package/test/browser/editing.check.mjs +2 -1
- package/test/browser/links-page.js +1 -0
- package/test/browser/links.check.mjs +20 -5
- package/test/browser/modeller.check.mjs +6 -0
- package/test/browser/more-open.js +11 -0
- package/test/browser/parts.check.mjs +11 -1
- package/test/browser/places-mesh.check.mjs +9 -3
- package/test/browser/product-page.check.mjs +9 -4
- package/test/browser/props.check.mjs +224 -0
- package/test/browser/screen-page.js +17 -0
- package/test/browser/screen.check.mjs +171 -29
- package/test/browser/section-page.js +1 -0
- package/test/browser/section.check.mjs +6 -0
- package/test/command-preview.test.ts +2 -2
- package/test/fasten-geometry.test.ts +15 -0
- package/test/fields.test.ts +4 -2
- package/test/figure-assistant-surface.test.ts +38 -0
- package/test/first-minute.test.ts +15 -2
- package/test/layout.test.ts +1 -1
- package/test/preview-size.test.ts +33 -0
- package/test/refusals.test.ts +1 -1
- package/test/resize-with-placement.test.ts +1 -1
- package/test/stage-model.test.ts +11 -1
- package/test/verdict.test.ts +7 -0
- package/translations/en.json +43 -27
- package/translations/es.json +24 -8
- package/translations/ja.json +25 -9
- package/translations/ko.json +56 -40
- package/translations/ms.json +25 -9
- package/translations/zh.json +25 -9
package/client/modeller/links.ts
CHANGED
|
@@ -134,7 +134,9 @@ export function partSummaryOf(
|
|
|
134
134
|
if (f.of === 'own') continue
|
|
135
135
|
if (f.of === 'instance') {
|
|
136
136
|
key = f.times === 1 ? 'figure.text.summary-size-follows' : 'figure.text.summary-size-share'
|
|
137
|
-
|
|
137
|
+
/* A measure following the figure's own size on its own axis says no measure word, so the axes that do so are one
|
|
138
|
+
sentence -- 「너비 · 높이 · 깊이는 도형 크기를 따라 바뀝니다」, not three (user 2026-10-16). */
|
|
139
|
+
params = f.times === 1 && f.axis === axis && !f.plus ? {} : { pct: pct(f.times), whole: say(`figure.label.measure-${f.axis}`) }
|
|
138
140
|
if (f.plus) (key = key + '-plus'), (params.plus = String(f.plus))
|
|
139
141
|
} else if (f.of === 'part') {
|
|
140
142
|
const other = say(`figure.label.measure-${f.axis}`)
|
|
@@ -675,6 +677,25 @@ export function motionFormRefusalOf(m: MotionForm, follow: string): 'multiplier'
|
|
|
675
677
|
}
|
|
676
678
|
|
|
677
679
|
/** The select's value for what a dimension follows now. */
|
|
680
|
+
/**
|
|
681
|
+
* What a measure that is not its own follows, in the words of the choice that set it -- 「도형 X 크기에 비례」, 「bed의 X」 --
|
|
682
|
+
* where the row once said only 「다른 규칙에 연결됨」 (user 2026-10-16: which rule?).
|
|
683
|
+
*/
|
|
684
|
+
export function followWordsOf(follow: Follow, say: (key: string, params?: Record<string, string>) => string): string {
|
|
685
|
+
switch (follow.of) {
|
|
686
|
+
case 'own':
|
|
687
|
+
return say('figure.label.follows-nothing')
|
|
688
|
+
case 'instance':
|
|
689
|
+
return say(follow.times === 1 ? 'figure.label.follows-instance' : 'figure.label.follows-instance-share', { axis: follow.axis.toUpperCase() })
|
|
690
|
+
case 'part':
|
|
691
|
+
return say('figure.label.follows-part-axis', { part: follow.part, axis: follow.axis.toUpperCase() })
|
|
692
|
+
case 'input':
|
|
693
|
+
return follow.input
|
|
694
|
+
case 'expression':
|
|
695
|
+
return say('figure.label.follows-expression')
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
678
699
|
export function choiceOf(follow: Follow): string {
|
|
679
700
|
switch (follow.of) {
|
|
680
701
|
case 'own':
|
|
@@ -35,6 +35,12 @@ export class ModellerKeys implements ReactiveController {
|
|
|
35
35
|
if (!this.of.taking()) return
|
|
36
36
|
const canvas = this.of.scene()
|
|
37
37
|
const path = e.composedPath()
|
|
38
|
+
/*
|
|
39
|
+
A key for something open over the work -- a group's 「⋯」 menu, the view or the history, a question -- is that
|
|
40
|
+
thing's: heard here first, Esc deselected the part and was kept from the menu, which then could not be closed by
|
|
41
|
+
the keyboard (2026-10-16).
|
|
42
|
+
*/
|
|
43
|
+
if (path.some(node => node instanceof Element && (node.localName.startsWith('ox-popup') || node.getAttribute('role') === 'dialog'))) return
|
|
38
44
|
const action = keyActionOf(
|
|
39
45
|
{
|
|
40
46
|
key: e.key,
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { figureRowOf, FIGURE_INSTANCE_SIZE_INPUTS, type FigureAsset } from '@hatiolab/figure-model'
|
|
2
|
+
|
|
3
|
+
type Say = (key: string, params?: Record<string, string>) => string
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* What the preview's one figure is at the size being tried, in a line the author reads beside it -- its size in
|
|
7
|
+
* millimetres and how many copies each row holds there (first 15 minutes, 2026-10-16: the author stretched the conveyor
|
|
8
|
+
* and could not tell that the rollers grew in number). The counts are figure-model's reading of what the figure draws at
|
|
9
|
+
* that size (`figureRowOf`), never worked out again here.
|
|
10
|
+
*/
|
|
11
|
+
export function previewSizeWordsOf(asset: FigureAsset, size: { x: number; y: number; z: number }, say: Say): string {
|
|
12
|
+
const mm = (v: number) => Math.round(v).toLocaleString()
|
|
13
|
+
const words = [`${mm(size.x)} × ${mm(size.y)} × ${mm(size.z)} mm`]
|
|
14
|
+
for (const row of rowCountsAt(asset, size)) words.push(say('figure.text.preview-copies', { part: row.part, n: String(row.count) }))
|
|
15
|
+
return words.join(' · ')
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Each row's count at the size given, in the order the parts are listed. */
|
|
19
|
+
export function rowCountsAt(asset: FigureAsset, size: { x: number; y: number; z: number }): Array<{ part: string; count: number }> {
|
|
20
|
+
const values = { [FIGURE_INSTANCE_SIZE_INPUTS.x]: size.x, [FIGURE_INSTANCE_SIZE_INPUTS.y]: size.y, [FIGURE_INSTANCE_SIZE_INPUTS.z]: size.z }
|
|
21
|
+
const rows: Array<{ part: string; count: number }> = []
|
|
22
|
+
for (const look of asset.appearance) {
|
|
23
|
+
let reading: ReturnType<typeof figureRowOf>
|
|
24
|
+
try {
|
|
25
|
+
reading = figureRowOf(asset, look.target, values)
|
|
26
|
+
} catch {
|
|
27
|
+
reading = undefined
|
|
28
|
+
}
|
|
29
|
+
if (reading) rows.push({ part: reading.part, count: reading.count })
|
|
30
|
+
}
|
|
31
|
+
return rows
|
|
32
|
+
}
|
|
@@ -14,6 +14,15 @@ import {
|
|
|
14
14
|
} from '@hatiolab/figure-model'
|
|
15
15
|
|
|
16
16
|
/** Consent to let clips go, and the figure and version it was given on. */
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A request the session refuses, with its code: a screen says it from the code (`sessionRefusalOf`), never from the
|
|
20
|
+
* message, which stays for logs (ADR-0054; figure audit 2026-10-16 N6 -- an AI card put the message in its note).
|
|
21
|
+
*/
|
|
22
|
+
function refusal(code: string, message: string): Error & { code: string } {
|
|
23
|
+
return Object.assign(Error(message), { code })
|
|
24
|
+
}
|
|
25
|
+
|
|
17
26
|
export interface FigureClipConsentFor extends FigureClipConsent {
|
|
18
27
|
context: string
|
|
19
28
|
revision: number
|
|
@@ -110,13 +119,18 @@ export function createFigureProductSession(store: FigureProductStore) {
|
|
|
110
119
|
}
|
|
111
120
|
return { dropClips: given.dropClips }
|
|
112
121
|
}
|
|
113
|
-
|
|
122
|
+
/* A model open to read: the draft is read during a save too -- the dock reads it when the person sends (audit N5). */
|
|
123
|
+
function readable() {
|
|
114
124
|
const view = workspace.read()
|
|
115
|
-
if (loading || error || !view.context || view.revision === null) throw
|
|
116
|
-
// Serializing writes also makes create retries safe: no second create while the first is pending.
|
|
117
|
-
if (saving) throw Error('저장이 끝난 뒤 편집해 주세요.')
|
|
125
|
+
if (loading || error || !view.context || view.revision === null) throw refusal('SESSION_NOT_OPEN', '먼저 모델을 정상적으로 열어 주세요.')
|
|
118
126
|
return { context: view.context, expectedRevision: view.revision, origin: 'ui' as const }
|
|
119
127
|
}
|
|
128
|
+
function writable() {
|
|
129
|
+
const open = readable()
|
|
130
|
+
// Serializing writes also makes create retries safe: no second create while the first is pending.
|
|
131
|
+
if (saving) throw refusal('SESSION_SAVING', '저장이 끝난 뒤 편집해 주세요.')
|
|
132
|
+
return open
|
|
133
|
+
}
|
|
120
134
|
function clear() {
|
|
121
135
|
workspace.close({ discardDesignChanges: true })
|
|
122
136
|
id = undefined
|
|
@@ -179,7 +193,7 @@ export function createFigureProductSession(store: FigureProductStore) {
|
|
|
179
193
|
const control = writable()
|
|
180
194
|
// AI must supply the context AND revision captured when it started. Never substitute current values.
|
|
181
195
|
if (origin === 'ai' && (context === undefined || expectedRevision === undefined))
|
|
182
|
-
throw
|
|
196
|
+
throw refusal('SESSION_VERSION_MISSING', 'AI 편집에는 요청 당시의 모델 문맥과 리비전이 필요합니다.')
|
|
183
197
|
return tracked('do', () =>
|
|
184
198
|
workspace.apply({
|
|
185
199
|
...control,
|
|
@@ -206,8 +220,8 @@ export function createFigureProductSession(store: FigureProductStore) {
|
|
|
206
220
|
* new figure not yet touched -- one being made is the person's, and keeps its own.
|
|
207
221
|
*/
|
|
208
222
|
createUnder(key: string) {
|
|
209
|
-
if (id || workspace.read().dirty) throw
|
|
210
|
-
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
|
|
223
|
+
if (id || workspace.read().dirty) throw refusal('SESSION_NEW_FIGURE_TOUCHED', '손대지 않은 새 모델에만 쓸 수 있습니다.')
|
|
224
|
+
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 refusal('SESSION_PROPOSAL_KEY', '제안 식별자가 올바르지 않습니다.')
|
|
211
225
|
creationId = key
|
|
212
226
|
},
|
|
213
227
|
/** The AI proposals whose steps are in the model now, each once. */
|
|
@@ -229,7 +243,7 @@ export function createFigureProductSession(store: FigureProductStore) {
|
|
|
229
243
|
/** What a command would do, on a copy (ADR-0097 §5.5): nothing is written, and the answer names its version. */
|
|
230
244
|
previewCommand(action: FigureAuthoringAction) {
|
|
231
245
|
const view = workspace.read()
|
|
232
|
-
if (loading || error || !view.context || view.revision === null) throw
|
|
246
|
+
if (loading || error || !view.context || view.revision === null) throw refusal('SESSION_NOT_OPEN', '먼저 모델을 정상적으로 열어 주세요.')
|
|
233
247
|
return workspace.previewCommand({ context: view.context, action })
|
|
234
248
|
},
|
|
235
249
|
/** One authoring command — what a person means, not a graph edit — as one step of the same history. */
|
|
@@ -256,7 +270,7 @@ export function createFigureProductSession(store: FigureProductStore) {
|
|
|
256
270
|
return tracked('redo', () => workspace.redo(writable()))
|
|
257
271
|
},
|
|
258
272
|
source() {
|
|
259
|
-
const { context } =
|
|
273
|
+
const { context } = readable()
|
|
260
274
|
return workspace.export({ context }).text
|
|
261
275
|
},
|
|
262
276
|
/**
|
|
@@ -226,8 +226,22 @@ function documentLimitRefusalOf(error: unknown, say: Say): string | null {
|
|
|
226
226
|
* the kernel's English on the screen (the duplicate of a row's part showed 「EDIT_TARGET: … not a repeated template」,
|
|
227
227
|
* 2026-09-27).
|
|
228
228
|
*/
|
|
229
|
+
/** The modeller session's own refusals (`product-session.ts`), by their code. Nothing for any other refusal. */
|
|
230
|
+
const SESSION_REFUSALS: Record<string, string> = {
|
|
231
|
+
SESSION_NOT_OPEN: 'figure.text.session-not-open',
|
|
232
|
+
SESSION_SAVING: 'figure.text.session-saving',
|
|
233
|
+
SESSION_VERSION_MISSING: 'figure.text.session-version-missing',
|
|
234
|
+
SESSION_NEW_FIGURE_TOUCHED: 'figure.text.session-new-figure-touched',
|
|
235
|
+
SESSION_PROPOSAL_KEY: 'figure.text.session-proposal-key'
|
|
236
|
+
}
|
|
237
|
+
export function sessionRefusalOf(error: unknown, say: Say): string | null {
|
|
238
|
+
const key = SESSION_REFUSALS[codeOf(error) ?? '']
|
|
239
|
+
return key ? say(key, {}) : null
|
|
240
|
+
}
|
|
241
|
+
|
|
229
242
|
export function editRefusalOf(error: unknown, say: Say): string {
|
|
230
243
|
return (
|
|
244
|
+
sessionRefusalOf(error, say) ??
|
|
231
245
|
sizeRangeRefusalOf(error, say) ??
|
|
232
246
|
inputRangeRefusalOf(error, say) ??
|
|
233
247
|
repeatPitchRefusalOf(error, say) ??
|
|
@@ -191,6 +191,17 @@ export function boardFor(parts: readonly FigureRenderPart[], at: Board & { depth
|
|
|
191
191
|
}
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
+
/**
|
|
195
|
+
* Whether the figure's reach has changed so far since the view was last fitted that the camera should fit it again:
|
|
196
|
+
* more than twice, or less than half, on any axis (first 15 minutes, 2026-10-16: a 100 mm box typed to 3000 × 800 × 600
|
|
197
|
+
* put the camera inside it, and the author saw grey until pressing 「화면에 맞추기」). A drag that nudges a part does not
|
|
198
|
+
* move the camera.
|
|
199
|
+
*/
|
|
200
|
+
export function needsRefit(fitted: (Board & { depth: number }) | undefined, now: Board & { depth: number }): boolean {
|
|
201
|
+
if (!fitted) return false
|
|
202
|
+
return (['width', 'height', 'depth'] as const).some(key => now[key] > fitted[key] * 2 || now[key] < fitted[key] / 2)
|
|
203
|
+
}
|
|
204
|
+
|
|
194
205
|
/**
|
|
195
206
|
* 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
207
|
* `componentOf` reads it). For a field a rule sets -- a fastened part's height, a turn its joint fixes -- the screen shows
|
|
@@ -6,11 +6,13 @@ 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,
|
|
9
|
+
import { fetchFigure, fetchFigureTypeNames, updateFigure } from '../graphql/index.js'
|
|
10
|
+
import { figureProposalOutcome } from '../figure-assistant.js'
|
|
10
11
|
import { availableRecipes, recipeGuide } from '../modeller/recipe-source.js'
|
|
11
12
|
import type { RecipeTemplate } from '../modeller/recipes.js'
|
|
12
13
|
import { occupancyFromParts } from '../modeller/occupancy.js'
|
|
13
14
|
import type { FigureAuthoringAction } from '@hatiolab/figure-model'
|
|
15
|
+
import type { FigureGateFinding } from '../types.js'
|
|
14
16
|
import '../modeller/editor.js'
|
|
15
17
|
import '@material/web/icon/icon.js'
|
|
16
18
|
import '../modeller/figure-history-panel.js'
|
|
@@ -30,7 +32,7 @@ import {
|
|
|
30
32
|
import type { FirstSave } from '../modeller/first-save.js'
|
|
31
33
|
import { typeOf } from '../modeller/first-save.js'
|
|
32
34
|
import { editRefusalOf } from '../modeller/refusals.js'
|
|
33
|
-
import {
|
|
35
|
+
import { askingToLeave, takeFigureOffer, takePendingNewFigure } from '../modeller/ai-proposal.js'
|
|
34
36
|
|
|
35
37
|
/**
|
|
36
38
|
* The product page has exactly one authoritative workspace for UI and AI input transactions.
|
|
@@ -118,6 +120,8 @@ export class FigureModellerPage extends PageView {
|
|
|
118
120
|
@state() private typeError = ''
|
|
119
121
|
/** Why a command from the side did nothing. Shown beside the tabs — the editor stays, since nothing in it was lost. */
|
|
120
122
|
@state() private commandError = ''
|
|
123
|
+
/** What the verdict says blocks release now, handed to the editor's bar. */
|
|
124
|
+
@state() private releaseBlockers: FigureGateFinding[] = []
|
|
121
125
|
/** The verdict's count, handed to the editor's verdict tab. */
|
|
122
126
|
@state() private instances?: number
|
|
123
127
|
@state() private budget?: FigureBudgetLine
|
|
@@ -152,8 +156,7 @@ export class FigureModellerPage extends PageView {
|
|
|
152
156
|
detail: () => (this.session.read().dirty ? i18next.t('figure.text.unsaved-changes') : ''),
|
|
153
157
|
anchor: () => ({ anchorType: 'figure', anchorId: id }),
|
|
154
158
|
/* The card's "check the result": the server's record, never the dock's memory; nothing is applied again. */
|
|
155
|
-
proposalOutcome:
|
|
156
|
-
proposalOutcomeFrom(await fetchProposalOutcome(proposalKey), (key, params) => i18next.t(key, params)),
|
|
159
|
+
proposalOutcome: figureProposalOutcome,
|
|
157
160
|
hostContext: () => ({
|
|
158
161
|
surface: 'figure-modeller',
|
|
159
162
|
editorMode: this.session.read().id ? 'existing' : 'new',
|
|
@@ -389,6 +392,8 @@ export class FigureModellerPage extends PageView {
|
|
|
389
392
|
The shell asks this while the edited model is still on screen. The app's own prompt, as every operato screen
|
|
390
393
|
asks -- not the browser's confirm box (user, 2026-09-24).
|
|
391
394
|
*/
|
|
395
|
+
/* A new figure the dock is opening waits for this answer, not against a clock (audit N4). */
|
|
396
|
+
const answered = askingToLeave()
|
|
392
397
|
this.discardApproved = !!(await OxPrompt.open({
|
|
393
398
|
type: 'warning',
|
|
394
399
|
title: i18next.t('figure.text.unsaved-changes'),
|
|
@@ -396,6 +401,7 @@ export class FigureModellerPage extends PageView {
|
|
|
396
401
|
confirmButton: { text: i18next.t('figure.button.leave-without-saving') },
|
|
397
402
|
cancelButton: { text: i18next.t('figure.button.keep-editing') }
|
|
398
403
|
}))
|
|
404
|
+
answered?.(this.discardApproved)
|
|
399
405
|
return this.discardApproved
|
|
400
406
|
}
|
|
401
407
|
async pageUpdated(changes: Record<string, unknown>, lifecycle: { resourceId?: string }) {
|
|
@@ -471,6 +477,8 @@ export class FigureModellerPage extends PageView {
|
|
|
471
477
|
the rail matches the page name and marks "new figure" while an existing one is being edited.
|
|
472
478
|
*/
|
|
473
479
|
menu: view.id ? 'figure-list' : undefined,
|
|
480
|
+
/* The canvas needs the width: the rail folds while the modeller is open and comes back as it was on leaving. */
|
|
481
|
+
rail: 'collapsed',
|
|
474
482
|
actions: [],
|
|
475
483
|
/*
|
|
476
484
|
No heading line: the canvas is what this page is for, and the name is already in the properties' first section
|
|
@@ -484,7 +492,7 @@ export class FigureModellerPage extends PageView {
|
|
|
484
492
|
if (this.error) return html`<p role="alert" class="error">${this.error}</p>`
|
|
485
493
|
const view = this.session.read()
|
|
486
494
|
/* 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). */
|
|
487
|
-
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}>
|
|
495
|
+
return html`<figure-editor .session=${this.session} .releaseState=${this.row} .releaseBlockers=${this.releaseBlockers} .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}>
|
|
488
496
|
${this.commandError ? html`<p slot="notice" fit-error role="alert">${this.commandError}</p>` : ''}
|
|
489
497
|
<figure-properties
|
|
490
498
|
slot="figure"
|
|
@@ -496,7 +504,7 @@ export class FigureModellerPage extends PageView {
|
|
|
496
504
|
@catalog=${this.onCatalog}
|
|
497
505
|
>
|
|
498
506
|
<label slot="type" type-field>
|
|
499
|
-
<span>${i18next.t('figure.label.figure-
|
|
507
|
+
<span>${i18next.t('figure.label.figure-identifier')}</span>
|
|
500
508
|
${this.row.version > 0
|
|
501
509
|
? html`<code>${this.row.type}</code><small>${i18next.t('figure.text.type-name-cannot-change')}</small>`
|
|
502
510
|
: html`<input
|
|
@@ -517,6 +525,7 @@ export class FigureModellerPage extends PageView {
|
|
|
517
525
|
this.budget = event.detail.budget
|
|
518
526
|
}}
|
|
519
527
|
@fit-base-to-parts=${this.fitToParts}
|
|
528
|
+
@release-verdict=${(event: CustomEvent<{ blocking: FigureGateFinding[] }>) => (this.releaseBlockers = event.detail.blocking)}
|
|
520
529
|
></figure-verdict>
|
|
521
530
|
<figure-history-panel
|
|
522
531
|
slot="history"
|
|
@@ -1 +1,7 @@
|
|
|
1
1
|
export declare const FIGURE_CANDIDATE_TOOL = "proposeFigureCandidate";
|
|
2
|
+
/**
|
|
3
|
+
* The dock's "check the result" of a figure proposal, on every screen that offers its card: what the server recorded of
|
|
4
|
+
* its save (ADR-0097 §5.6 item 5). The modeller asks it, and so does an app's home where a new figure's card is made --
|
|
5
|
+
* there it was missing, and a card cut off by a reload stayed "no answer" for good (figure audit 2026-10-16 N8).
|
|
6
|
+
*/
|
|
7
|
+
export declare function figureProposalOutcome(proposalKey: string): Promise<import("./modeller/ai-proposal.js").FigureProposalOutcome | undefined>;
|
|
@@ -11,8 +11,16 @@ import { navigate } from '@operato/shell';
|
|
|
11
11
|
import { registerProposalHandler } from '@things-factory/ai-assistant/dist-client/index.js';
|
|
12
12
|
import { fetchProposalOutcome } from './graphql/index.js';
|
|
13
13
|
import { modellerPathOf } from './modeller/modeller-route.js';
|
|
14
|
-
import { pendNewFigure, runFigureProposal } from './modeller/ai-proposal.js';
|
|
14
|
+
import { pendNewFigure, proposalOutcomeFrom, runFigureProposal } from './modeller/ai-proposal.js';
|
|
15
15
|
export const FIGURE_CANDIDATE_TOOL = 'proposeFigureCandidate';
|
|
16
|
+
/**
|
|
17
|
+
* The dock's "check the result" of a figure proposal, on every screen that offers its card: what the server recorded of
|
|
18
|
+
* its save (ADR-0097 §5.6 item 5). The modeller asks it, and so does an app's home where a new figure's card is made --
|
|
19
|
+
* there it was missing, and a card cut off by a reload stayed "no answer" for good (figure audit 2026-10-16 N8).
|
|
20
|
+
*/
|
|
21
|
+
export async function figureProposalOutcome(proposalKey) {
|
|
22
|
+
return proposalOutcomeFrom(await fetchProposalOutcome(proposalKey), (key, params) => i18next.t(key, params));
|
|
23
|
+
}
|
|
16
24
|
registerProposalHandler(FIGURE_CANDIDATE_TOOL, async (proposal, context) => {
|
|
17
25
|
const outcome = await runFigureProposal(proposal, {
|
|
18
26
|
offer(detail) {
|
|
@@ -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,oBAAoB,EAAE,MAAM,oBAAoB,CAAA;AACzD,OAAO,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAA;AAC7D,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAA0B,MAAM,2BAA2B,CAAA;
|
|
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,oBAAoB,EAAE,MAAM,oBAAoB,CAAA;AACzD,OAAO,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAA;AAC7D,OAAO,EAAE,aAAa,EAAE,mBAAmB,EAAE,iBAAiB,EAA0B,MAAM,2BAA2B,CAAA;AAEzH,MAAM,CAAC,MAAM,qBAAqB,GAAG,wBAAwB,CAAA;AAE7D;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CAAC,WAAmB;IAC7D,OAAO,mBAAmB,CAAC,MAAM,oBAAoB,CAAC,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC,CAAA;AAC9G,CAAC;AAED,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;QACD,SAAS,EAAE,oBAAoB;QAC/B,SAAS,CAAC,EAAE;YACV,QAAQ,CAAC,cAAc,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAA;QAClC,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 { fetchProposalOutcome } from './graphql/index.js'\nimport { modellerPathOf } from './modeller/modeller-route.js'\nimport { pendNewFigure, proposalOutcomeFrom, runFigureProposal, type FigureApplyDetail } from './modeller/ai-proposal.js'\n\nexport const FIGURE_CANDIDATE_TOOL = 'proposeFigureCandidate'\n\n/**\n * The dock's \"check the result\" of a figure proposal, on every screen that offers its card: what the server recorded of\n * its save (ADR-0097 §5.6 item 5). The modeller asks it, and so does an app's home where a new figure's card is made --\n * there it was missing, and a card cut off by a reload stayed \"no answer\" for good (figure audit 2026-10-16 N8).\n */\nexport async function figureProposalOutcome(proposalKey: string) {\n return proposalOutcomeFrom(await fetchProposalOutcome(proposalKey), (key, params) => i18next.t(key, params))\n}\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 savedInto: fetchProposalOutcome,\n openSaved(id) {\n navigate(modellerPathOf({ id }))\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"]}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The parts a release finding points at, by the author's names. The server's `at` lists the first word of each path it
|
|
3
|
+
* could not prove -- a part's name, or a section of the format such as 「occupancy」 -- and the screen showed it as it came
|
|
4
|
+
* (「occupancy · roller-row-1-roller」, user 2026-10-16). Only the figure's own part names are kept.
|
|
5
|
+
*/
|
|
6
|
+
export declare function affectedPartsOf(at: string, parts: readonly string[]): string[];
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The parts a release finding points at, by the author's names. The server's `at` lists the first word of each path it
|
|
3
|
+
* could not prove -- a part's name, or a section of the format such as 「occupancy」 -- and the screen showed it as it came
|
|
4
|
+
* (「occupancy · roller-row-1-roller」, user 2026-10-16). Only the figure's own part names are kept.
|
|
5
|
+
*/
|
|
6
|
+
export function affectedPartsOf(at, parts) {
|
|
7
|
+
return at
|
|
8
|
+
.split('·')
|
|
9
|
+
.map(word => word.trim())
|
|
10
|
+
.filter(word => parts.includes(word));
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=affected-parts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"affected-parts.js","sourceRoot":"","sources":["../../client/modeller/affected-parts.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,EAAU,EAAE,KAAwB;IAClE,OAAO,EAAE;SACN,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;SACxB,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAA;AACzC,CAAC","sourcesContent":["/**\n * The parts a release finding points at, by the author's names. The server's `at` lists the first word of each path it\n * could not prove -- a part's name, or a section of the format such as 「occupancy」 -- and the screen showed it as it came\n * (「occupancy · roller-row-1-roller」, user 2026-10-16). Only the figure's own part names are kept.\n */\nexport function affectedPartsOf(at: string, parts: readonly string[]): string[] {\n return at\n .split('·')\n .map(word => word.trim())\n .filter(word => parts.includes(word))\n}\n"]}
|
|
@@ -19,6 +19,8 @@ export interface FigureApplyDetail extends FigureRefusalRecord {
|
|
|
19
19
|
key?: string;
|
|
20
20
|
steps: FigureProposalStep[];
|
|
21
21
|
create: boolean;
|
|
22
|
+
/** The figure a revision was made for: the one the model read. */
|
|
23
|
+
figureId?: string;
|
|
22
24
|
context?: string;
|
|
23
25
|
revision?: number;
|
|
24
26
|
applied?: boolean;
|
|
@@ -72,18 +74,26 @@ export declare function takeFigureOffer(session: FigureProductSession, detail: F
|
|
|
72
74
|
say: Say;
|
|
73
75
|
}): boolean;
|
|
74
76
|
export declare function runFigureProposal(card: FigureProposalCard, target: FigureProposalTarget, say: Say): Promise<FigureProposalOutcome>;
|
|
77
|
+
type Settle = (result: {
|
|
78
|
+
applied: boolean;
|
|
79
|
+
error?: string;
|
|
80
|
+
code?: string;
|
|
81
|
+
}) => void;
|
|
75
82
|
export declare function pendNewFigure(steps: FigureProposalStep[], key?: string, ms?: number): Promise<{
|
|
76
83
|
applied: boolean;
|
|
77
84
|
error?: string;
|
|
78
85
|
code?: string;
|
|
79
86
|
}>;
|
|
87
|
+
/**
|
|
88
|
+
* For a page about to ask the person whether to leave unsaved edits: the new figure waiting to open stops its clock
|
|
89
|
+
* while they answer -- an answer is no absent page (figure audit 2026-10-16 N4). Give it the answer: leaving runs the
|
|
90
|
+
* clock again for the page that opens; staying says now that it was not opened. Nothing waiting: undefined.
|
|
91
|
+
*/
|
|
92
|
+
export declare function askingToLeave(): ((leaving: boolean) => void) | undefined;
|
|
80
93
|
/** For the page, on a new figure it has just started: the steps waiting for it, and where to say how it went. */
|
|
81
94
|
export declare function takePendingNewFigure(): {
|
|
82
95
|
steps: FigureProposalStep[];
|
|
83
96
|
key?: string;
|
|
84
|
-
settle:
|
|
85
|
-
applied: boolean;
|
|
86
|
-
error?: string;
|
|
87
|
-
code?: string;
|
|
88
|
-
}) => void;
|
|
97
|
+
settle: Settle;
|
|
89
98
|
} | undefined;
|
|
99
|
+
export {};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { clipLossOf, refusalRecordOf } from './refusals.js';
|
|
1
|
+
import { clipLossOf, editRefusalOf, refusalRecordOf } from './refusals.js';
|
|
2
2
|
/**
|
|
3
3
|
* The dock's "check the result" (ADR-0097 §5.6 item 5): what the server recorded of a proposal. Saved into figures:
|
|
4
4
|
* accepted, named. Nothing recorded: not known -- applied and not saved yet, undone, or never taken -- so `undefined`,
|
|
@@ -25,7 +25,8 @@ function refused(refusal, say) {
|
|
|
25
25
|
? (error ?? '')
|
|
26
26
|
: clips
|
|
27
27
|
? say('figure.text.proposal-would-drop-clips', { clips: clips.map(c => c.clip).join(', ') })
|
|
28
|
-
:
|
|
28
|
+
: /* A refusal with a code is said from it, as the modeller says an edit it refuses; never its message (audit N6). */
|
|
29
|
+
say('figure.text.proposal-refused', { reason: code ? editRefusalOf(refusal, (key, params) => say(key, params)) : (error ?? '') });
|
|
29
30
|
return { verdict: 'refused', errorCode: code ?? 'refused', note };
|
|
30
31
|
}
|
|
31
32
|
/**
|
|
@@ -38,6 +39,13 @@ export function takeFigureOffer(session, detail, { onSavedFigure, say }) {
|
|
|
38
39
|
const view = session.read();
|
|
39
40
|
if (detail.create && onSavedFigure)
|
|
40
41
|
return false;
|
|
42
|
+
/*
|
|
43
|
+
A revision is for the figure the model read; with another open it is left untaken, and the handler says it needs that
|
|
44
|
+
figure open. The version is no proof of the figure: two figures opened in two page loads stood at the same one
|
|
45
|
+
(figure audit 2026-10-16 N3).
|
|
46
|
+
*/
|
|
47
|
+
if (!detail.create && typeof detail.figureId === 'string' && detail.figureId !== (view.id ?? view.creationId))
|
|
48
|
+
return false;
|
|
41
49
|
if (detail.create && view.dirty) {
|
|
42
50
|
/* A new figure already being made is the person's; a proposal does not go on top of it or replace it. */
|
|
43
51
|
detail.applied = false;
|
|
@@ -71,7 +79,7 @@ export function takeFigureOffer(session, detail, { onSavedFigure, say }) {
|
|
|
71
79
|
export async function runFigureProposal(card, target, say) {
|
|
72
80
|
const steps = card?.steps;
|
|
73
81
|
if (!Array.isArray(steps) || !steps.length)
|
|
74
|
-
return { verdict: 'unavailable', errorCode: 'no-steps' };
|
|
82
|
+
return { verdict: 'unavailable', errorCode: 'no-steps', note: say('figure.text.proposal-empty') };
|
|
75
83
|
const create = card.create === true;
|
|
76
84
|
/*
|
|
77
85
|
A new figure's proposal already saved into a figure is not made again: that figure is opened (ADR-0061 decision 3,
|
|
@@ -89,7 +97,7 @@ export async function runFigureProposal(card, target, say) {
|
|
|
89
97
|
applied: steps.length,
|
|
90
98
|
note: say('figure.text.proposal-applied', { n: String(steps.length) })
|
|
91
99
|
});
|
|
92
|
-
const detail = target.offer({ key: card.key, steps, create, context: card.context, revision: card.revision });
|
|
100
|
+
const detail = target.offer({ key: card.key, steps, create, figureId: create ? undefined : card.figureId, context: card.context, revision: card.revision });
|
|
93
101
|
if (detail.applied)
|
|
94
102
|
return accepted();
|
|
95
103
|
if (detail.applied === false)
|
|
@@ -104,15 +112,12 @@ export async function runFigureProposal(card, target, say) {
|
|
|
104
112
|
? { verdict: 'unavailable', errorCode: 'not-opened', note: say('figure.text.proposal-not-opened') }
|
|
105
113
|
: refused(opened, say);
|
|
106
114
|
}
|
|
107
|
-
/*
|
|
108
|
-
A new figure's proposal handed to the modeller page across a navigation: the handler leaves it here and opens the
|
|
109
|
-
modeller on a new figure; the page takes it once it has started that figure. Left untaken, it lapses -- a later new
|
|
110
|
-
figure must not receive steps nobody is waiting for.
|
|
111
|
-
*/
|
|
112
115
|
let pending;
|
|
113
116
|
export function pendNewFigure(steps, key, ms = 10_000) {
|
|
114
117
|
pending?.settle({ applied: false, code: 'not-opened' });
|
|
115
118
|
return new Promise(resolve => {
|
|
119
|
+
let timer;
|
|
120
|
+
const lapse = () => mine.settle({ applied: false, code: 'not-opened' });
|
|
116
121
|
const mine = {
|
|
117
122
|
steps,
|
|
118
123
|
key,
|
|
@@ -121,12 +126,36 @@ export function pendNewFigure(steps, key, ms = 10_000) {
|
|
|
121
126
|
if (pending === mine)
|
|
122
127
|
pending = undefined;
|
|
123
128
|
resolve(result);
|
|
129
|
+
},
|
|
130
|
+
hold: () => clearTimeout(timer),
|
|
131
|
+
resume: () => {
|
|
132
|
+
clearTimeout(timer);
|
|
133
|
+
timer = setTimeout(lapse, ms);
|
|
124
134
|
}
|
|
125
135
|
};
|
|
126
|
-
|
|
136
|
+
mine.resume();
|
|
127
137
|
pending = mine;
|
|
128
138
|
});
|
|
129
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* For a page about to ask the person whether to leave unsaved edits: the new figure waiting to open stops its clock
|
|
142
|
+
* while they answer -- an answer is no absent page (figure audit 2026-10-16 N4). Give it the answer: leaving runs the
|
|
143
|
+
* clock again for the page that opens; staying says now that it was not opened. Nothing waiting: undefined.
|
|
144
|
+
*/
|
|
145
|
+
export function askingToLeave() {
|
|
146
|
+
const waiting = pending;
|
|
147
|
+
if (!waiting)
|
|
148
|
+
return undefined;
|
|
149
|
+
waiting.hold();
|
|
150
|
+
return leaving => {
|
|
151
|
+
if (pending !== waiting)
|
|
152
|
+
return;
|
|
153
|
+
if (leaving)
|
|
154
|
+
waiting.resume();
|
|
155
|
+
else
|
|
156
|
+
waiting.settle({ applied: false, code: 'not-opened' });
|
|
157
|
+
};
|
|
158
|
+
}
|
|
130
159
|
/** For the page, on a new figure it has just started: the steps waiting for it, and where to say how it went. */
|
|
131
160
|
export function takePendingNewFigure() {
|
|
132
161
|
const taken = pending;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ai-proposal.js","sourceRoot":"","sources":["../../client/modeller/ai-proposal.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * An AI proposal reaching the modeller (ADR-0094). The dock's card carries the modeller's own commands; running it\n * applies them as one step of the modeller's history, undone whole, on the version the model saw. Nothing half made\n * is applied, and every outcome is said: applied, refused with the reason, or not possible here.\n *\n * Kept free of Lit, i18n and the shell so node tests read it directly; the handler and the page pass in what they own.\n */\nimport type { FigureProposalStep } from '@hatiolab/figure-model'\nimport type { FigureProductSession } from './product-session.js'\nimport { clipLossOf, refusalRecordOf, type FigureRefusalRecord } from './refusals.js'\n\n/** What the card carries, as `proposeFigureCandidate` returned it. */\nexport interface FigureProposalCard {\n /** The proposal's own name, given by the tool: a save records it, a new figure made from it is created under it. */\n key?: string\n steps?: FigureProposalStep[]\n create?: boolean\n figureId?: string\n context?: string\n revision?: number\n}\n\n/**\n * The `figure-apply` event's detail. The page that takes it sets `applied`, and when refused `error` and the refusal in\n * figure-model's shape (`code`, `details`, the `clips` it would stop).\n */\nexport interface FigureApplyDetail extends FigureRefusalRecord {\n key?: string\n steps: FigureProposalStep[]\n create: boolean\n context?: string\n revision?: number\n applied?: boolean\n error?: string\n}\n\nexport type Say = (key: string, params?: Record<string, string>) => string\n\nexport interface FigureProposalTarget {\n /** Offers the proposal to a modeller on screen. It comes back untouched when none took it. */\n offer(detail: FigureApplyDetail): FigureApplyDetail\n /** Opens a new figure in the modeller and applies the steps there; resolves once the page has, or has not. */\n openNew(steps: FigureProposalStep[], key?: string): Promise<{ applied: boolean; error?: string; code?: string }>\n /** The figures the server recorded a save of this proposal into (`figureProposalOutcome`), first saved first. */\n savedInto(key: string): Promise<ReadonlyArray<{ figureId: string; type?: string; name?: string }>>\n /** Opens a saved figure in the modeller. */\n openSaved(figureId: string): void\n}\n\nexport interface FigureProposalOutcome {\n verdict: 'accepted' | 'refused' | 'unavailable'\n errorCode?: string\n applied?: number\n note?: string\n}\n\n/**\n * The dock's \"check the result\" (ADR-0097 §5.6 item 5): what the server recorded of a proposal. Saved into figures:\n * accepted, named. Nothing recorded: not known -- applied and not saved yet, undone, or never taken -- so `undefined`,\n * which the dock keeps as \"not known\" and never turns into a refusal. Nothing is run again.\n */\nexport function proposalOutcomeFrom(saved: ReadonlyArray<{ figureId: string; type?: string; name?: string }>, say: Say): FigureProposalOutcome | undefined {\n if (!saved.length) return undefined\n const names = saved.map(f => f.name || f.type || f.figureId).join(', ')\n return { verdict: 'accepted', note: say('figure.text.proposal-saved-into', { names }) }\n}\n\n/** The workspace refuses a request made on another version; said as what happened, not as the kernel's code. */\nconst STALE = new Set(['REVISION_CONFLICT', 'CONTEXT_CONFLICT'])\n\nfunction refused(refusal: FigureRefusalRecord & { error?: string }, say: Say): FigureProposalOutcome {\n const { code, error } = refusal\n const clips = clipLossOf(refusal)\n /*\n A proposal that would stop clips is refused whole; nobody but the person may agree to that, and a proposal is not\n asked about -- the person edits by hand if they mean it (designer 2026-09-27).\n */\n const note =\n code && STALE.has(code)\n ? say('figure.text.proposal-stale')\n : code === 'NEW_FIGURE_UNSAVED'\n ? (error ?? '')\n : clips\n ? say('figure.text.proposal-would-drop-clips', { clips: clips.map(c => c.clip).join(', ') })\n : say('figure.text.proposal-refused', { reason: error ?? '' })\n return { verdict: 'refused', errorCode: code ?? 'refused', note }\n}\n\n/**\n * The modeller page's way of taking an offer from the dock (`figure-apply`): input changes, or a proposal's commands\n * as one step (ADR-0094). A revision is applied on the version the model saw; a new figure's proposal only on a new\n * figure not yet touched, and a page on a saved figure leaves it untaken. Never with a consent to let clips go: a\n * proposal that would stop one is refused whole, the clips named (designer 2026-09-27). True when it was applied.\n */\nexport function takeFigureOffer(\n session: FigureProductSession,\n detail: FigureApplyDetail & { changes?: unknown },\n { onSavedFigure, say }: { onSavedFigure: boolean; say: Say }\n): boolean {\n const view = session.read()\n if (detail.create && onSavedFigure) return false\n if (detail.create && view.dirty) {\n /* A new figure already being made is the person's; a proposal does not go on top of it or replace it. */\n detail.applied = false\n detail.code = 'NEW_FIGURE_UNSAVED'\n detail.error = say('figure.text.proposal-new-figure-unsaved')\n return false\n }\n try {\n if (detail.create) {\n /*\n Taken on the new figure the modeller has open, whose conversation asked for it: the figure keeps its own creation\n id, the anchor of that conversation (ADR-0061 decision 3, ruled 2026-10-16). One proposal stays one figure by the\n save's record of it, which `runFigureProposal` reads before the card runs again.\n */\n session.applyProposal(detail.steps, view.revision!, view.context!, detail.key)\n } else if (detail.steps) session.applyProposal(detail.steps, detail.revision!, detail.context!, detail.key)\n else session.apply(detail.changes as never, 'ai', detail.revision, detail.context)\n detail.applied = true\n return true\n } catch (error) {\n detail.applied = false\n Object.assign(detail, refusalRecordOf(error))\n detail.error = error instanceof Error ? error.message : String(error)\n return false\n }\n}\n\nexport async function runFigureProposal(card: FigureProposalCard, target: FigureProposalTarget, say: Say): Promise<FigureProposalOutcome> {\n const steps = card?.steps\n if (!Array.isArray(steps) || !steps.length) return { verdict: 'unavailable', errorCode: 'no-steps' }\n const create = card.create === true\n /*\n A new figure's proposal already saved into a figure is not made again: that figure is opened (ADR-0061 decision 3,\n ruled 2026-10-16). Taken on a new figure that kept its own creation id, the save's record is what says it was made.\n */\n if (create && card.key) {\n const saved = await target.savedInto(card.key)\n if (saved.length) {\n target.openSaved(saved[0]!.figureId)\n return proposalOutcomeFrom(saved, say)!\n }\n }\n const accepted = (): FigureProposalOutcome => ({\n verdict: 'accepted',\n applied: steps.length,\n note: say('figure.text.proposal-applied', { n: String(steps.length) })\n })\n\n const detail = target.offer({ key: card.key, steps, create, context: card.context, revision: card.revision })\n if (detail.applied) return accepted()\n if (detail.applied === false) return refused(detail, say)\n\n /* A revision belongs to the figure the model saw; with that modeller gone there is nowhere true to apply it. */\n if (!create) return { verdict: 'unavailable', errorCode: 'modeller-not-open', note: say('figure.text.proposal-needs-modeller') }\n\n const opened = await target.openNew(steps, card.key)\n if (opened.applied) return accepted()\n return opened.code === 'not-opened'\n ? { verdict: 'unavailable', errorCode: 'not-opened', note: say('figure.text.proposal-not-opened') }\n : refused(opened, say)\n}\n\n/*\n A new figure's proposal handed to the modeller page across a navigation: the handler leaves it here and opens the\n modeller on a new figure; the page takes it once it has started that figure. Left untaken, it lapses -- a later new\n figure must not receive steps nobody is waiting for.\n*/\nlet pending: { steps: FigureProposalStep[]; key?: string; settle: (result: { applied: boolean; error?: string; code?: string }) => void } | undefined\n\nexport function pendNewFigure(steps: FigureProposalStep[], key?: string, ms = 10_000): Promise<{ applied: boolean; error?: string; code?: string }> {\n pending?.settle({ applied: false, code: 'not-opened' })\n return new Promise(resolve => {\n const mine = {\n steps,\n key,\n settle: (result: { applied: boolean; error?: string; code?: string }) => {\n clearTimeout(timer)\n if (pending === mine) pending = undefined\n resolve(result)\n }\n }\n const timer = setTimeout(() => mine.settle({ applied: false, code: 'not-opened' }), ms)\n pending = mine\n })\n}\n\n/** For the page, on a new figure it has just started: the steps waiting for it, and where to say how it went. */\nexport function takePendingNewFigure():\n | { steps: FigureProposalStep[]; key?: string; settle: (result: { applied: boolean; error?: string; code?: string }) => void }\n | undefined {\n const taken = pending\n pending = undefined\n return taken\n}\n"]}
|
|
1
|
+
{"version":3,"file":"ai-proposal.js","sourceRoot":"","sources":["../../client/modeller/ai-proposal.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * An AI proposal reaching the modeller (ADR-0094). The dock's card carries the modeller's own commands; running it\n * applies them as one step of the modeller's history, undone whole, on the version the model saw. Nothing half made\n * is applied, and every outcome is said: applied, refused with the reason, or not possible here.\n *\n * Kept free of Lit, i18n and the shell so node tests read it directly; the handler and the page pass in what they own.\n */\nimport type { FigureProposalStep } from '@hatiolab/figure-model'\nimport type { FigureProductSession } from './product-session.js'\nimport { clipLossOf, editRefusalOf, refusalRecordOf, type FigureRefusalRecord } from './refusals.js'\n\n/** What the card carries, as `proposeFigureCandidate` returned it. */\nexport interface FigureProposalCard {\n /** The proposal's own name, given by the tool: a save records it, a new figure made from it is created under it. */\n key?: string\n steps?: FigureProposalStep[]\n create?: boolean\n figureId?: string\n context?: string\n revision?: number\n}\n\n/**\n * The `figure-apply` event's detail. The page that takes it sets `applied`, and when refused `error` and the refusal in\n * figure-model's shape (`code`, `details`, the `clips` it would stop).\n */\nexport interface FigureApplyDetail extends FigureRefusalRecord {\n key?: string\n steps: FigureProposalStep[]\n create: boolean\n /** The figure a revision was made for: the one the model read. */\n figureId?: string\n context?: string\n revision?: number\n applied?: boolean\n error?: string\n}\n\nexport type Say = (key: string, params?: Record<string, string>) => string\n\nexport interface FigureProposalTarget {\n /** Offers the proposal to a modeller on screen. It comes back untouched when none took it. */\n offer(detail: FigureApplyDetail): FigureApplyDetail\n /** Opens a new figure in the modeller and applies the steps there; resolves once the page has, or has not. */\n openNew(steps: FigureProposalStep[], key?: string): Promise<{ applied: boolean; error?: string; code?: string }>\n /** The figures the server recorded a save of this proposal into (`figureProposalOutcome`), first saved first. */\n savedInto(key: string): Promise<ReadonlyArray<{ figureId: string; type?: string; name?: string }>>\n /** Opens a saved figure in the modeller. */\n openSaved(figureId: string): void\n}\n\nexport interface FigureProposalOutcome {\n verdict: 'accepted' | 'refused' | 'unavailable'\n errorCode?: string\n applied?: number\n note?: string\n}\n\n/**\n * The dock's \"check the result\" (ADR-0097 §5.6 item 5): what the server recorded of a proposal. Saved into figures:\n * accepted, named. Nothing recorded: not known -- applied and not saved yet, undone, or never taken -- so `undefined`,\n * which the dock keeps as \"not known\" and never turns into a refusal. Nothing is run again.\n */\nexport function proposalOutcomeFrom(saved: ReadonlyArray<{ figureId: string; type?: string; name?: string }>, say: Say): FigureProposalOutcome | undefined {\n if (!saved.length) return undefined\n const names = saved.map(f => f.name || f.type || f.figureId).join(', ')\n return { verdict: 'accepted', note: say('figure.text.proposal-saved-into', { names }) }\n}\n\n/** The workspace refuses a request made on another version; said as what happened, not as the kernel's code. */\nconst STALE = new Set(['REVISION_CONFLICT', 'CONTEXT_CONFLICT'])\n\nfunction refused(refusal: FigureRefusalRecord & { error?: string }, say: Say): FigureProposalOutcome {\n const { code, error } = refusal\n const clips = clipLossOf(refusal)\n /*\n A proposal that would stop clips is refused whole; nobody but the person may agree to that, and a proposal is not\n asked about -- the person edits by hand if they mean it (designer 2026-09-27).\n */\n const note =\n code && STALE.has(code)\n ? say('figure.text.proposal-stale')\n : code === 'NEW_FIGURE_UNSAVED'\n ? (error ?? '')\n : clips\n ? say('figure.text.proposal-would-drop-clips', { clips: clips.map(c => c.clip).join(', ') })\n : /* A refusal with a code is said from it, as the modeller says an edit it refuses; never its message (audit N6). */\n say('figure.text.proposal-refused', { reason: code ? editRefusalOf(refusal, (key, params) => say(key, params)) : (error ?? '') })\n return { verdict: 'refused', errorCode: code ?? 'refused', note }\n}\n\n/**\n * The modeller page's way of taking an offer from the dock (`figure-apply`): input changes, or a proposal's commands\n * as one step (ADR-0094). A revision is applied on the version the model saw; a new figure's proposal only on a new\n * figure not yet touched, and a page on a saved figure leaves it untaken. Never with a consent to let clips go: a\n * proposal that would stop one is refused whole, the clips named (designer 2026-09-27). True when it was applied.\n */\nexport function takeFigureOffer(\n session: FigureProductSession,\n detail: FigureApplyDetail & { changes?: unknown },\n { onSavedFigure, say }: { onSavedFigure: boolean; say: Say }\n): boolean {\n const view = session.read()\n if (detail.create && onSavedFigure) return false\n /*\n A revision is for the figure the model read; with another open it is left untaken, and the handler says it needs that\n figure open. The version is no proof of the figure: two figures opened in two page loads stood at the same one\n (figure audit 2026-10-16 N3).\n */\n if (!detail.create && typeof detail.figureId === 'string' && detail.figureId !== (view.id ?? view.creationId)) return false\n if (detail.create && view.dirty) {\n /* A new figure already being made is the person's; a proposal does not go on top of it or replace it. */\n detail.applied = false\n detail.code = 'NEW_FIGURE_UNSAVED'\n detail.error = say('figure.text.proposal-new-figure-unsaved')\n return false\n }\n try {\n if (detail.create) {\n /*\n Taken on the new figure the modeller has open, whose conversation asked for it: the figure keeps its own creation\n id, the anchor of that conversation (ADR-0061 decision 3, ruled 2026-10-16). One proposal stays one figure by the\n save's record of it, which `runFigureProposal` reads before the card runs again.\n */\n session.applyProposal(detail.steps, view.revision!, view.context!, detail.key)\n } else if (detail.steps) session.applyProposal(detail.steps, detail.revision!, detail.context!, detail.key)\n else session.apply(detail.changes as never, 'ai', detail.revision, detail.context)\n detail.applied = true\n return true\n } catch (error) {\n detail.applied = false\n Object.assign(detail, refusalRecordOf(error))\n detail.error = error instanceof Error ? error.message : String(error)\n return false\n }\n}\n\nexport async function runFigureProposal(card: FigureProposalCard, target: FigureProposalTarget, say: Say): Promise<FigureProposalOutcome> {\n const steps = card?.steps\n if (!Array.isArray(steps) || !steps.length) return { verdict: 'unavailable', errorCode: 'no-steps', note: say('figure.text.proposal-empty') }\n const create = card.create === true\n /*\n A new figure's proposal already saved into a figure is not made again: that figure is opened (ADR-0061 decision 3,\n ruled 2026-10-16). Taken on a new figure that kept its own creation id, the save's record is what says it was made.\n */\n if (create && card.key) {\n const saved = await target.savedInto(card.key)\n if (saved.length) {\n target.openSaved(saved[0]!.figureId)\n return proposalOutcomeFrom(saved, say)!\n }\n }\n const accepted = (): FigureProposalOutcome => ({\n verdict: 'accepted',\n applied: steps.length,\n note: say('figure.text.proposal-applied', { n: String(steps.length) })\n })\n\n const detail = target.offer({ key: card.key, steps, create, figureId: create ? undefined : card.figureId, context: card.context, revision: card.revision })\n if (detail.applied) return accepted()\n if (detail.applied === false) return refused(detail, say)\n\n /* A revision belongs to the figure the model saw; with that modeller gone there is nowhere true to apply it. */\n if (!create) return { verdict: 'unavailable', errorCode: 'modeller-not-open', note: say('figure.text.proposal-needs-modeller') }\n\n const opened = await target.openNew(steps, card.key)\n if (opened.applied) return accepted()\n return opened.code === 'not-opened'\n ? { verdict: 'unavailable', errorCode: 'not-opened', note: say('figure.text.proposal-not-opened') }\n : refused(opened, say)\n}\n\n/*\n A new figure's proposal handed to the modeller page across a navigation: the handler leaves it here and opens the\n modeller on a new figure; the page takes it once it has started that figure. Left untaken, it lapses -- a later new\n figure must not receive steps nobody is waiting for.\n*/\ntype Settle = (result: { applied: boolean; error?: string; code?: string }) => void\nlet pending: { steps: FigureProposalStep[]; key?: string; settle: Settle; hold(): void; resume(): void } | undefined\n\nexport function pendNewFigure(steps: FigureProposalStep[], key?: string, ms = 10_000): Promise<{ applied: boolean; error?: string; code?: string }> {\n pending?.settle({ applied: false, code: 'not-opened' })\n return new Promise(resolve => {\n let timer: ReturnType<typeof setTimeout> | undefined\n const lapse = () => mine.settle({ applied: false, code: 'not-opened' })\n const mine = {\n steps,\n key,\n settle: (result: { applied: boolean; error?: string; code?: string }) => {\n clearTimeout(timer)\n if (pending === mine) pending = undefined\n resolve(result)\n },\n hold: () => clearTimeout(timer),\n resume: () => {\n clearTimeout(timer)\n timer = setTimeout(lapse, ms)\n }\n }\n mine.resume()\n pending = mine\n })\n}\n\n/**\n * For a page about to ask the person whether to leave unsaved edits: the new figure waiting to open stops its clock\n * while they answer -- an answer is no absent page (figure audit 2026-10-16 N4). Give it the answer: leaving runs the\n * clock again for the page that opens; staying says now that it was not opened. Nothing waiting: undefined.\n */\nexport function askingToLeave(): ((leaving: boolean) => void) | undefined {\n const waiting = pending\n if (!waiting) return undefined\n waiting.hold()\n return leaving => {\n if (pending !== waiting) return\n if (leaving) waiting.resume()\n else waiting.settle({ applied: false, code: 'not-opened' })\n }\n}\n\n/** For the page, on a new figure it has just started: the steps waiting for it, and where to say how it went. */\nexport function takePendingNewFigure(): { steps: FigureProposalStep[]; key?: string; settle: Settle } | undefined {\n const taken = pending\n pending = undefined\n return taken\n}\n"]}
|
|
@@ -34,6 +34,9 @@ export declare class FigureCanvas extends LitElement {
|
|
|
34
34
|
private stage;
|
|
35
35
|
private scene?;
|
|
36
36
|
private board?;
|
|
37
|
+
/** The figure's reach when the view was last fitted to it. */
|
|
38
|
+
private fittedFor?;
|
|
39
|
+
private refitTimer?;
|
|
37
40
|
private standingFor?;
|
|
38
41
|
private watcher?;
|
|
39
42
|
/** Set while this canvas writes to components, so the scene's echo of it is not taken for a drag. */
|