@things-factory/figure-ui 10.1.64 → 10.1.66
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 +26 -10
- package/client/graphql/index.ts +0 -58
- package/client/modeller/v3-ai-proposal.ts +111 -0
- package/client/modeller/v3-editor.ts +3 -2
- package/client/modeller/v3-product-session.ts +10 -1
- package/client/modeller/v3-refusals.ts +17 -0
- package/client/pages/figure-v3-modeller-page.ts +35 -2
- package/client/types.ts +0 -39
- package/dist-client/figure-assistant.js +22 -10
- package/dist-client/figure-assistant.js.map +1 -1
- package/dist-client/graphql/index.d.ts +1 -19
- package/dist-client/graphql/index.js +0 -46
- package/dist-client/graphql/index.js.map +1 -1
- package/dist-client/modeller/figure-v3-capability.d.ts +1 -1
- package/dist-client/modeller/figure-v3-clips.d.ts +1 -1
- package/dist-client/modeller/figure-v3-controls.d.ts +1 -1
- package/dist-client/modeller/figure-v3-links.d.ts +1 -1
- package/dist-client/modeller/figure-v3-material.d.ts +1 -1
- package/dist-client/modeller/figure-v3-properties.d.ts +1 -1
- package/dist-client/modeller/v3-ai-proposal.d.ts +51 -0
- package/dist-client/modeller/v3-ai-proposal.js +64 -0
- package/dist-client/modeller/v3-ai-proposal.js.map +1 -0
- package/dist-client/modeller/v3-clips.d.ts +1 -1
- package/dist-client/modeller/v3-editor.js +3 -2
- package/dist-client/modeller/v3-editor.js.map +1 -1
- package/dist-client/modeller/v3-product-session.d.ts +35 -1
- package/dist-client/modeller/v3-product-session.js +8 -0
- package/dist-client/modeller/v3-product-session.js.map +1 -1
- package/dist-client/modeller/v3-refusals.d.ts +6 -0
- package/dist-client/modeller/v3-refusals.js +18 -0
- package/dist-client/modeller/v3-refusals.js.map +1 -1
- package/dist-client/pages/figure-v3-modeller-page.d.ts +7 -1
- package/dist-client/pages/figure-v3-modeller-page.js +41 -2
- package/dist-client/pages/figure-v3-modeller-page.js.map +1 -1
- package/dist-client/tsconfig.fui.tsbuildinfo +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/types.d.ts +0 -44
- package/dist-client/types.js.map +1 -1
- package/package.json +4 -4
- package/test/figure-assistant-surface.test.ts +9 -6
- package/test/v3-ai-proposal.test.ts +112 -0
- package/test/v3-refusals.test.ts +12 -0
- package/translations/en.json +8 -1
- package/translations/ja.json +8 -1
- package/translations/ko.json +8 -1
- package/translations/ms.json +8 -1
- package/translations/zh.json +8 -1
- package/client/modeller/proposal-session.ts +0 -98
- package/client/modeller/proposal.ts +0 -171
- package/dist-client/modeller/proposal-session.d.ts +0 -28
- package/dist-client/modeller/proposal-session.js +0 -66
- package/dist-client/modeller/proposal-session.js.map +0 -1
- package/dist-client/modeller/proposal.d.ts +0 -45
- package/dist-client/modeller/proposal.js +0 -114
- package/dist-client/modeller/proposal.js.map +0 -1
- package/test/ai-proposal-contract.test.ts +0 -563
|
@@ -1,20 +1,36 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* What the AI dock does with a figure
|
|
2
|
+
* What the AI dock does with a figure proposal - figure-ui declares it because figure-service declares the tool
|
|
3
3
|
* (`proposeFigureCandidate`, design/ai/shared-dock.md §4).
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* said to the person in words, not a silent pass - until a proposer writes V3 changes, which the modeller takes
|
|
9
|
-
* through `figure-v3-apply`.
|
|
5
|
+
* A proposal is a list of the modeller's own commands (ADR-0094). Running it applies them in the modeller as one step
|
|
6
|
+
* of its history: a revision on the figure open there, at the version the model saw; a new figure on a new figure the
|
|
7
|
+
* modeller opens. The outcome is said in words on every branch (`v3-ai-proposal.ts`).
|
|
10
8
|
*/
|
|
11
9
|
import { i18next } from '@operato/i18n'
|
|
10
|
+
import { navigate } from '@operato/shell'
|
|
12
11
|
import { registerProposalHandler } from '@things-factory/ai-assistant/dist-client/index.js'
|
|
13
12
|
|
|
13
|
+
import { modellerPathOf } from './modeller/modeller-route.js'
|
|
14
|
+
import { pendNewFigure, runFigureProposal, type FigureApplyDetail } from './modeller/v3-ai-proposal.js'
|
|
15
|
+
|
|
14
16
|
export const FIGURE_CANDIDATE_TOOL = 'proposeFigureCandidate'
|
|
15
17
|
|
|
16
|
-
registerProposalHandler(FIGURE_CANDIDATE_TOOL, async (
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
registerProposalHandler(FIGURE_CANDIDATE_TOOL, async (proposal, context) => {
|
|
19
|
+
const outcome = await runFigureProposal(
|
|
20
|
+
proposal,
|
|
21
|
+
{
|
|
22
|
+
offer(detail: FigureApplyDetail) {
|
|
23
|
+
window.dispatchEvent(new CustomEvent('figure-v3-apply', { detail }))
|
|
24
|
+
return detail
|
|
25
|
+
},
|
|
26
|
+
openNew(steps) {
|
|
27
|
+
const opened = pendNewFigure(steps)
|
|
28
|
+
navigate(modellerPathOf())
|
|
29
|
+
return opened
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
(key, params) => i18next.t(key, params)
|
|
33
|
+
)
|
|
34
|
+
if (outcome.note) context.notify(outcome.verdict === 'accepted' ? 'info' : 'error', outcome.note)
|
|
35
|
+
return outcome
|
|
20
36
|
})
|
package/client/graphql/index.ts
CHANGED
|
@@ -8,8 +8,6 @@ import type {
|
|
|
8
8
|
FigureInspection,
|
|
9
9
|
FigureListResult,
|
|
10
10
|
FigurePatch,
|
|
11
|
-
FigureProposal,
|
|
12
|
-
ProposalFeedback,
|
|
13
11
|
FigureVersion,
|
|
14
12
|
NewFigure
|
|
15
13
|
} from '../types.js'
|
|
@@ -187,62 +185,6 @@ export async function deleteFigure(id: string): Promise<boolean> {
|
|
|
187
185
|
return unwrap<boolean>(response, 'deleteFigure')
|
|
188
186
|
}
|
|
189
187
|
|
|
190
|
-
/**
|
|
191
|
-
* 저작 보조에게 후보를 시킨다.
|
|
192
|
-
*
|
|
193
|
-
* 팔레트를 **클라이언트가 보낸다.** 서버는 팔레트를 모른다 — things-scene 이 갖고
|
|
194
|
-
* 있는데 그 패키지의 node 조건은 팔레트가 없는 번들로 간다. 그리는 쪽이 어떤 토큰을
|
|
195
|
-
* 풀 수 있는지 아는 유일한 자리이므로 그쪽이 보낸다.
|
|
196
|
-
*
|
|
197
|
-
* 돌아오는 것은 **후보**다. 저장되지 않았다 — 저작자가 받아야 정본이 된다.
|
|
198
|
-
*/
|
|
199
|
-
export async function proposeFigure(request: {
|
|
200
|
-
prompt: string
|
|
201
|
-
base?: string
|
|
202
|
-
type?: string
|
|
203
|
-
palette: string[]
|
|
204
|
-
refine?: boolean
|
|
205
|
-
feedback?: ProposalFeedback[]
|
|
206
|
-
image?: File
|
|
207
|
-
}): Promise<FigureProposal> {
|
|
208
|
-
const response = await client.mutate({
|
|
209
|
-
mutation: gql`
|
|
210
|
-
mutation ProposeFigure($request: ProposeRequest!) {
|
|
211
|
-
proposeFigure(request: $request) {
|
|
212
|
-
source
|
|
213
|
-
score
|
|
214
|
-
grade
|
|
215
|
-
triangles
|
|
216
|
-
groups
|
|
217
|
-
quality {
|
|
218
|
-
status
|
|
219
|
-
summary
|
|
220
|
-
findings {
|
|
221
|
-
dimension
|
|
222
|
-
code
|
|
223
|
-
message
|
|
224
|
-
}
|
|
225
|
-
visualRegions
|
|
226
|
-
visualCoverage
|
|
227
|
-
visualReadability
|
|
228
|
-
}
|
|
229
|
-
attempts
|
|
230
|
-
violations {
|
|
231
|
-
code
|
|
232
|
-
message
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
`,
|
|
237
|
-
variables: { request },
|
|
238
|
-
// @operato/graphql 는 이 신호가 있을 때만 Upload가 포함된 변수를 multipart로 인코딩한다.
|
|
239
|
-
// 없으면 브라우저 File은 JSON에서 {}가 되어 서버 Upload scalar가 요청 자체를 거절한다.
|
|
240
|
-
context: { hasUpload: !!request.image }
|
|
241
|
-
})
|
|
242
|
-
|
|
243
|
-
return unwrap<FigureProposal>(response, 'proposeFigure')
|
|
244
|
-
}
|
|
245
|
-
|
|
246
188
|
/**
|
|
247
189
|
* 발행한다 — 판 번호가 오르고, 그 순간이 판본으로 남는다.
|
|
248
190
|
*
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* An AI proposal reaching the modeller (ADR-0094). The dock's card carries the modeller's own commands; running it
|
|
3
|
+
* applies them as one step of the modeller's history, undone whole, on the version the model saw. Nothing half made
|
|
4
|
+
* is applied, and every outcome is said: applied, refused with the reason, or not possible here.
|
|
5
|
+
*
|
|
6
|
+
* Kept free of Lit, i18n and the shell so node tests read it directly; the handler and the page pass in what they own.
|
|
7
|
+
*/
|
|
8
|
+
import type { V3ProposalStep } from '@hatiolab/figure-model'
|
|
9
|
+
|
|
10
|
+
/** What the card carries, as `proposeFigureCandidate` returned it. */
|
|
11
|
+
export interface FigureProposalCard {
|
|
12
|
+
steps?: V3ProposalStep[]
|
|
13
|
+
create?: boolean
|
|
14
|
+
figureId?: string
|
|
15
|
+
context?: string
|
|
16
|
+
revision?: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** The `figure-v3-apply` event's detail. The page that takes it sets `applied`, and `error` when refused. */
|
|
20
|
+
export interface FigureApplyDetail {
|
|
21
|
+
steps: V3ProposalStep[]
|
|
22
|
+
create: boolean
|
|
23
|
+
context?: string
|
|
24
|
+
revision?: number
|
|
25
|
+
applied?: boolean
|
|
26
|
+
error?: string
|
|
27
|
+
code?: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type Say = (key: string, params?: Record<string, string>) => string
|
|
31
|
+
|
|
32
|
+
export interface FigureProposalTarget {
|
|
33
|
+
/** Offers the proposal to a modeller on screen. It comes back untouched when none took it. */
|
|
34
|
+
offer(detail: FigureApplyDetail): FigureApplyDetail
|
|
35
|
+
/** Opens a new figure in the modeller and applies the steps there; resolves once the page has, or has not. */
|
|
36
|
+
openNew(steps: V3ProposalStep[]): Promise<{ applied: boolean; error?: string; code?: string }>
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface FigureProposalOutcome {
|
|
40
|
+
verdict: 'accepted' | 'refused' | 'unavailable'
|
|
41
|
+
errorCode?: string
|
|
42
|
+
applied?: number
|
|
43
|
+
note?: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The workspace refuses a request made on another version; said as what happened, not as the kernel's code. */
|
|
47
|
+
const STALE = new Set(['REVISION_CONFLICT', 'CONTEXT_CONFLICT'])
|
|
48
|
+
|
|
49
|
+
function refused(code: string | undefined, error: string | undefined, say: Say): FigureProposalOutcome {
|
|
50
|
+
const note =
|
|
51
|
+
code && STALE.has(code)
|
|
52
|
+
? say('figure.text.proposal-stale')
|
|
53
|
+
: code === 'NEW_FIGURE_UNSAVED'
|
|
54
|
+
? (error ?? '')
|
|
55
|
+
: say('figure.text.proposal-refused', { reason: error ?? '' })
|
|
56
|
+
return { verdict: 'refused', errorCode: code ?? 'refused', note }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function runFigureProposal(card: FigureProposalCard, target: FigureProposalTarget, say: Say): Promise<FigureProposalOutcome> {
|
|
60
|
+
const steps = card?.steps
|
|
61
|
+
if (!Array.isArray(steps) || !steps.length) return { verdict: 'unavailable', errorCode: 'no-steps' }
|
|
62
|
+
const create = card.create === true
|
|
63
|
+
const accepted = (): FigureProposalOutcome => ({
|
|
64
|
+
verdict: 'accepted',
|
|
65
|
+
applied: steps.length,
|
|
66
|
+
note: say('figure.text.proposal-applied', { n: String(steps.length) })
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
const detail = target.offer({ steps, create, context: card.context, revision: card.revision })
|
|
70
|
+
if (detail.applied) return accepted()
|
|
71
|
+
if (detail.applied === false) return refused(detail.code, detail.error, say)
|
|
72
|
+
|
|
73
|
+
/* A revision belongs to the figure the model saw; with that modeller gone there is nowhere true to apply it. */
|
|
74
|
+
if (!create) return { verdict: 'unavailable', errorCode: 'modeller-not-open', note: say('figure.text.proposal-needs-modeller') }
|
|
75
|
+
|
|
76
|
+
const opened = await target.openNew(steps)
|
|
77
|
+
if (opened.applied) return accepted()
|
|
78
|
+
return opened.code === 'not-opened'
|
|
79
|
+
? { verdict: 'unavailable', errorCode: 'not-opened', note: say('figure.text.proposal-not-opened') }
|
|
80
|
+
: refused(opened.code, opened.error, say)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/*
|
|
84
|
+
A new figure's proposal handed to the modeller page across a navigation: the handler leaves it here and opens the
|
|
85
|
+
modeller on a new figure; the page takes it once it has started that figure. Left untaken, it lapses -- a later new
|
|
86
|
+
figure must not receive steps nobody is waiting for.
|
|
87
|
+
*/
|
|
88
|
+
let pending: { steps: V3ProposalStep[]; settle: (result: { applied: boolean; error?: string; code?: string }) => void } | undefined
|
|
89
|
+
|
|
90
|
+
export function pendNewFigure(steps: V3ProposalStep[], ms = 10_000): Promise<{ applied: boolean; error?: string; code?: string }> {
|
|
91
|
+
pending?.settle({ applied: false, code: 'not-opened' })
|
|
92
|
+
return new Promise(resolve => {
|
|
93
|
+
const mine = {
|
|
94
|
+
steps,
|
|
95
|
+
settle: (result: { applied: boolean; error?: string; code?: string }) => {
|
|
96
|
+
clearTimeout(timer)
|
|
97
|
+
if (pending === mine) pending = undefined
|
|
98
|
+
resolve(result)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const timer = setTimeout(() => mine.settle({ applied: false, code: 'not-opened' }), ms)
|
|
102
|
+
pending = mine
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** For the page, on a new figure it has just started: the steps waiting for it, and where to say how it went. */
|
|
107
|
+
export function takePendingNewFigure(): { steps: V3ProposalStep[]; settle: (result: { applied: boolean; error?: string; code?: string }) => void } | undefined {
|
|
108
|
+
const taken = pending
|
|
109
|
+
pending = undefined
|
|
110
|
+
return taken
|
|
111
|
+
}
|
|
@@ -29,7 +29,7 @@ import type { V3PaletteKind, V3PartRow } from './figure-v3-parts.js'
|
|
|
29
29
|
import { paletteRecipe } from './v3-palette.js'
|
|
30
30
|
import type { FigureV3Canvas } from './v3-canvas.js'
|
|
31
31
|
import { nudgeOf } from './v3-stage-model.js'
|
|
32
|
-
import { inDeclaredRoom, narrowingRetryOf, partRefusalOf, placementRefusalOf, sizeRangeRefusalOf } from './v3-refusals.js'
|
|
32
|
+
import { inDeclaredRoom, inputRangeRefusalOf, narrowingRetryOf, partRefusalOf, placementRefusalOf, sizeRangeRefusalOf } from './v3-refusals.js'
|
|
33
33
|
import type { V3RenderPart } from './v3-renderer.js'
|
|
34
34
|
import type { V3BudgetLine } from './v3-maturity.js'
|
|
35
35
|
import { instanceRangeOf, instanceRangeToDeclare, proportionToFollow, rangeDiffers, rangeWith, turnsAboutAJoint, valueNow } from './v3-links.js'
|
|
@@ -324,7 +324,8 @@ export class FigureV3Editor extends LitElement {
|
|
|
324
324
|
this.error = ''
|
|
325
325
|
this.changed()
|
|
326
326
|
} catch (error) {
|
|
327
|
-
|
|
327
|
+
const say = (key: string, params: Record<string, string>) => i18next.t(key, params)
|
|
328
|
+
this.error = sizeRangeRefusalOf(error, say) ?? inputRangeRefusalOf(error, say) ?? (error instanceof Error ? error.message : String(error))
|
|
328
329
|
} finally {
|
|
329
330
|
/* Committed or refused, the value in hand is gone: the figure as it stands is drawn again. */
|
|
330
331
|
this.live = NOTHING_LIVE
|
|
@@ -6,7 +6,8 @@ import {
|
|
|
6
6
|
type V3InputChange,
|
|
7
7
|
type V3PrimitiveCreation,
|
|
8
8
|
type V3PartEdit,
|
|
9
|
-
type V3AuthoringAction
|
|
9
|
+
type V3AuthoringAction,
|
|
10
|
+
type V3ProposalStep
|
|
10
11
|
} from '@hatiolab/figure-model'
|
|
11
12
|
|
|
12
13
|
export interface V3ProductRecord {
|
|
@@ -111,6 +112,14 @@ export function createV3ProductSession(store: V3ProductStore) {
|
|
|
111
112
|
changes
|
|
112
113
|
})
|
|
113
114
|
},
|
|
115
|
+
/**
|
|
116
|
+
* An AI proposal (ADR-0094): the modeller's commands, as one step of the history, on the version the model saw.
|
|
117
|
+
* Never on the current one instead -- a person's edit in between makes the proposal stale, and it is refused.
|
|
118
|
+
*/
|
|
119
|
+
applyProposal(steps: readonly V3ProposalStep[], expectedRevision: number, context: string) {
|
|
120
|
+
writable()
|
|
121
|
+
return workspace.applyProposal({ origin: 'ai', context, expectedRevision, steps })
|
|
122
|
+
},
|
|
114
123
|
add(part: V3PrimitiveCreation) {
|
|
115
124
|
return workspace.addPrimitive({ ...writable(), part })
|
|
116
125
|
},
|
|
@@ -74,3 +74,20 @@ export function partRefusalOf(error: unknown, part: string, say: Say): string |
|
|
|
74
74
|
if (follows) return say('figure.text.part-is-followed', { part, others: follows[1]! })
|
|
75
75
|
return say('figure.text.part-is-referenced', { part })
|
|
76
76
|
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* A value outside the range its input declares, with the range and the value given -- the kernel says
|
|
80
|
+
* 「size.x: 6021100 outside [300,1200]」. The figure's own size is named by its axis; any other input by its range only.
|
|
81
|
+
* The repeat pitch's refusal shares the code and has its own words (`figure.text.repeat-pitch-too-small`).
|
|
82
|
+
*/
|
|
83
|
+
export function inputRangeRefusalOf(error: unknown, say: Say): string | null {
|
|
84
|
+
if ((error as { code?: string })?.code !== 'INPUT_RANGE') return null
|
|
85
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
86
|
+
const found = /([\w.-]+): (-?[\d.e+]+) outside \[(-?[\d.e+]+),\s*(-?[\d.e+]+)\]/.exec(message)
|
|
87
|
+
if (!found) return null
|
|
88
|
+
const [, input, value, min, max] = found
|
|
89
|
+
const number = (text: string) => Number(text).toLocaleString(undefined, { maximumFractionDigits: 3 })
|
|
90
|
+
const axis = /^size\.([xyz])$/.exec(input!)?.[1]
|
|
91
|
+
const params = { value: number(value!), min: number(min!), max: number(max!) }
|
|
92
|
+
return axis ? say('figure.text.input-range-size', { ...params, axis: axis.toUpperCase() }) : say('figure.text.input-range', params)
|
|
93
|
+
}
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
} from '@things-factory/ai-assistant/dist-client/index.js'
|
|
26
26
|
import type { FirstSave } from '../modeller/v3-first-save.js'
|
|
27
27
|
import { placementRefusalOf } from '../modeller/v3-refusals.js'
|
|
28
|
+
import { takePendingNewFigure } from '../modeller/v3-ai-proposal.js'
|
|
28
29
|
|
|
29
30
|
/**
|
|
30
31
|
* The V3 product page has exactly one authoritative workspace for UI and AI input transactions.
|
|
@@ -158,6 +159,9 @@ export class FigureV3ModellerPage extends PageView {
|
|
|
158
159
|
editorMode: 'existing',
|
|
159
160
|
figureId: id,
|
|
160
161
|
draftSource: JSON.parse(this.session.source()),
|
|
162
|
+
/* The version the model sees; a proposal is applied on it, or refused if the figure moved on (ADR-0094). */
|
|
163
|
+
context: this.session.read().context,
|
|
164
|
+
revision: this.session.read().revision,
|
|
161
165
|
palette: Object.keys(activePalette())
|
|
162
166
|
})
|
|
163
167
|
})
|
|
@@ -297,16 +301,32 @@ export class FigureV3ModellerPage extends PageView {
|
|
|
297
301
|
this.changed()
|
|
298
302
|
this.readRow(id)
|
|
299
303
|
}
|
|
300
|
-
/**
|
|
304
|
+
/**
|
|
305
|
+
* The dock's way in (`figure-assistant.ts`): input changes, or a proposal's commands as one step (ADR-0094). A
|
|
306
|
+
* revision is applied on the version the model saw; a new figure's proposal only on a new figure not yet touched. A
|
|
307
|
+
* page on a saved figure leaves a new figure's proposal untaken, and the handler opens a new figure for it.
|
|
308
|
+
*/
|
|
301
309
|
private applyAI = (event: Event) => {
|
|
302
310
|
const detail = (event as CustomEvent).detail
|
|
303
311
|
if (!this.active || !detail) return
|
|
312
|
+
const view = this.session.read()
|
|
313
|
+
if (detail.create && this.routeId) return
|
|
314
|
+
if (detail.create && view.dirty) {
|
|
315
|
+
/* A new figure already being made is the person's; a proposal does not go on top of it or replace it. */
|
|
316
|
+
detail.applied = false
|
|
317
|
+
detail.code = 'NEW_FIGURE_UNSAVED'
|
|
318
|
+
detail.error = i18next.t('figure.text.proposal-new-figure-unsaved')
|
|
319
|
+
return
|
|
320
|
+
}
|
|
304
321
|
try {
|
|
305
|
-
this.session.
|
|
322
|
+
if (detail.create) this.session.applyProposal(detail.steps, view.revision!, view.context!)
|
|
323
|
+
else if (detail.steps) this.session.applyProposal(detail.steps, detail.revision, detail.context)
|
|
324
|
+
else this.session.apply(detail.changes, 'ai', detail.revision, detail.context)
|
|
306
325
|
detail.applied = true
|
|
307
326
|
this.changed()
|
|
308
327
|
} catch (error) {
|
|
309
328
|
detail.applied = false
|
|
329
|
+
detail.code = (error as { code?: string })?.code
|
|
310
330
|
detail.error = error instanceof Error ? error.message : String(error)
|
|
311
331
|
}
|
|
312
332
|
}
|
|
@@ -361,6 +381,7 @@ export class FigureV3ModellerPage extends PageView {
|
|
|
361
381
|
this.draft = undefined
|
|
362
382
|
this.row = { version: 0 }
|
|
363
383
|
this.typeError = ''
|
|
384
|
+
this.takeNewFigureProposal()
|
|
364
385
|
}
|
|
365
386
|
this.routeId = id
|
|
366
387
|
this.error = ''
|
|
@@ -369,6 +390,18 @@ export class FigureV3ModellerPage extends PageView {
|
|
|
369
390
|
this.error = error instanceof Error ? error.message : String(error)
|
|
370
391
|
}
|
|
371
392
|
}
|
|
393
|
+
/** A new figure the dock opened for a proposal: its steps, applied as the new figure's first step. */
|
|
394
|
+
private takeNewFigureProposal() {
|
|
395
|
+
const waiting = takePendingNewFigure()
|
|
396
|
+
if (!waiting) return
|
|
397
|
+
const view = this.session.read()
|
|
398
|
+
try {
|
|
399
|
+
this.session.applyProposal(waiting.steps, view.revision!, view.context!)
|
|
400
|
+
waiting.settle({ applied: true })
|
|
401
|
+
} catch (error) {
|
|
402
|
+
waiting.settle({ applied: false, code: (error as { code?: string })?.code, error: error instanceof Error ? error.message : String(error) })
|
|
403
|
+
}
|
|
404
|
+
}
|
|
372
405
|
/** The first save's question, over the names this domain already uses (V2 asked the same). */
|
|
373
406
|
private askFirstSave = async (name: string): Promise<FirstSave | undefined> => {
|
|
374
407
|
const taken = await fetchFigureTypeNames().catch(() => [] as string[])
|
package/client/types.ts
CHANGED
|
@@ -102,45 +102,6 @@ export interface FigureFinding {
|
|
|
102
102
|
at?: string
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
/**
|
|
106
|
-
* 저작 보조가 낸 후보.
|
|
107
|
-
*
|
|
108
|
-
* **정본이 아니다.** 저작자가 보고 받아야 정본이 된다 — 서버는 저장하지 않고
|
|
109
|
-
* 돌려주기만 한다.
|
|
110
|
-
*/
|
|
111
|
-
export interface FigureProposal {
|
|
112
|
-
/** 후보 정본. JSON 문자열이다 — 화면이 파싱한다. */
|
|
113
|
-
source: string
|
|
114
|
-
score: number
|
|
115
|
-
grade: string
|
|
116
|
-
triangles: number
|
|
117
|
-
/** 재질 묶음 = 인스턴스 하나가 무는 draw call. */
|
|
118
|
-
groups: number
|
|
119
|
-
/** Rule-based review of measurable design concerns; it is not an aesthetic verdict. */
|
|
120
|
-
quality: {
|
|
121
|
-
status: 'ready' | 'review'
|
|
122
|
-
summary: string
|
|
123
|
-
findings: Array<{ dimension: string; code: string; message: string }>
|
|
124
|
-
visualRegions: number
|
|
125
|
-
visualCoverage: number
|
|
126
|
-
visualReadability: number
|
|
127
|
-
}
|
|
128
|
-
/** 형식은 맞으나 정책을 넘은 것. 막지 않는다 — 저작자가 정한다. */
|
|
129
|
-
violations: FigureFinding[]
|
|
130
|
-
/** 몇 번 만에 됐나. 진단용이다. */
|
|
131
|
-
attempts: number
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/** 사람이 후보를 수락하거나 버릴 때 직접 남긴 현재 저작 세션의 참고 신호. */
|
|
135
|
-
export interface ProposalFeedback {
|
|
136
|
-
outcome: 'accepted' | 'discarded'
|
|
137
|
-
selectedChanges: number
|
|
138
|
-
totalChanges: number
|
|
139
|
-
note?: string
|
|
140
|
-
grade?: string
|
|
141
|
-
quality?: { status?: 'ready' | 'review'; findingCodes?: string[] }
|
|
142
|
-
}
|
|
143
|
-
|
|
144
105
|
/**
|
|
145
106
|
* 발행 판정 한 줄.
|
|
146
107
|
*
|
|
@@ -1,19 +1,31 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* What the AI dock does with a figure
|
|
2
|
+
* What the AI dock does with a figure proposal - figure-ui declares it because figure-service declares the tool
|
|
3
3
|
* (`proposeFigureCandidate`, design/ai/shared-dock.md §4).
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* said to the person in words, not a silent pass - until a proposer writes V3 changes, which the modeller takes
|
|
9
|
-
* through `figure-v3-apply`.
|
|
5
|
+
* A proposal is a list of the modeller's own commands (ADR-0094). Running it applies them in the modeller as one step
|
|
6
|
+
* of its history: a revision on the figure open there, at the version the model saw; a new figure on a new figure the
|
|
7
|
+
* modeller opens. The outcome is said in words on every branch (`v3-ai-proposal.ts`).
|
|
10
8
|
*/
|
|
11
9
|
import { i18next } from '@operato/i18n';
|
|
10
|
+
import { navigate } from '@operato/shell';
|
|
12
11
|
import { registerProposalHandler } from '@things-factory/ai-assistant/dist-client/index.js';
|
|
12
|
+
import { modellerPathOf } from './modeller/modeller-route.js';
|
|
13
|
+
import { pendNewFigure, runFigureProposal } from './modeller/v3-ai-proposal.js';
|
|
13
14
|
export const FIGURE_CANDIDATE_TOOL = 'proposeFigureCandidate';
|
|
14
|
-
registerProposalHandler(FIGURE_CANDIDATE_TOOL, async (
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
registerProposalHandler(FIGURE_CANDIDATE_TOOL, async (proposal, context) => {
|
|
16
|
+
const outcome = await runFigureProposal(proposal, {
|
|
17
|
+
offer(detail) {
|
|
18
|
+
window.dispatchEvent(new CustomEvent('figure-v3-apply', { detail }));
|
|
19
|
+
return detail;
|
|
20
|
+
},
|
|
21
|
+
openNew(steps) {
|
|
22
|
+
const opened = pendNewFigure(steps);
|
|
23
|
+
navigate(modellerPathOf());
|
|
24
|
+
return opened;
|
|
25
|
+
}
|
|
26
|
+
}, (key, params) => i18next.t(key, params));
|
|
27
|
+
if (outcome.note)
|
|
28
|
+
context.notify(outcome.verdict === 'accepted' ? 'info' : 'error', outcome.note);
|
|
29
|
+
return outcome;
|
|
18
30
|
});
|
|
19
31
|
//# sourceMappingURL=figure-assistant.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"figure-assistant.js","sourceRoot":"","sources":["../client/figure-assistant.ts"],"names":[],"mappings":"AAAA
|
|
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,8BAA8B,CAAA;AAEvG,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,iBAAiB,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC,CAAA;YACpE,OAAO,MAAM,CAAA;QACf,CAAC;QACD,OAAO,CAAC,KAAK;YACX,MAAM,MAAM,GAAG,aAAa,CAAC,KAAK,CAAC,CAAA;YACnC,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 (`v3-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/v3-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-v3-apply', { detail }))\n return detail\n },\n openNew(steps) {\n const opened = pendNewFigure(steps)\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"]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Figure, FigureFinding, FigureInspection, FigureListResult, FigurePatch,
|
|
1
|
+
import type { Figure, FigureFinding, FigureInspection, FigureListResult, FigurePatch, FigureVersion, NewFigure } from '../types.js';
|
|
2
2
|
/**
|
|
3
3
|
* 목록을 가져온다.
|
|
4
4
|
*
|
|
@@ -32,24 +32,6 @@ export interface SaveResult {
|
|
|
32
32
|
export declare function createFigure(figure: NewFigure): Promise<SaveResult>;
|
|
33
33
|
export declare function updateFigure(id: string, patch: FigurePatch): Promise<SaveResult>;
|
|
34
34
|
export declare function deleteFigure(id: string): Promise<boolean>;
|
|
35
|
-
/**
|
|
36
|
-
* 저작 보조에게 후보를 시킨다.
|
|
37
|
-
*
|
|
38
|
-
* 팔레트를 **클라이언트가 보낸다.** 서버는 팔레트를 모른다 — things-scene 이 갖고
|
|
39
|
-
* 있는데 그 패키지의 node 조건은 팔레트가 없는 번들로 간다. 그리는 쪽이 어떤 토큰을
|
|
40
|
-
* 풀 수 있는지 아는 유일한 자리이므로 그쪽이 보낸다.
|
|
41
|
-
*
|
|
42
|
-
* 돌아오는 것은 **후보**다. 저장되지 않았다 — 저작자가 받아야 정본이 된다.
|
|
43
|
-
*/
|
|
44
|
-
export declare function proposeFigure(request: {
|
|
45
|
-
prompt: string;
|
|
46
|
-
base?: string;
|
|
47
|
-
type?: string;
|
|
48
|
-
palette: string[];
|
|
49
|
-
refine?: boolean;
|
|
50
|
-
feedback?: ProposalFeedback[];
|
|
51
|
-
image?: File;
|
|
52
|
-
}): Promise<FigureProposal>;
|
|
53
35
|
/**
|
|
54
36
|
* 발행한다 — 판 번호가 오르고, 그 순간이 판본으로 남는다.
|
|
55
37
|
*
|
|
@@ -147,52 +147,6 @@ export async function deleteFigure(id) {
|
|
|
147
147
|
});
|
|
148
148
|
return unwrap(response, 'deleteFigure');
|
|
149
149
|
}
|
|
150
|
-
/**
|
|
151
|
-
* 저작 보조에게 후보를 시킨다.
|
|
152
|
-
*
|
|
153
|
-
* 팔레트를 **클라이언트가 보낸다.** 서버는 팔레트를 모른다 — things-scene 이 갖고
|
|
154
|
-
* 있는데 그 패키지의 node 조건은 팔레트가 없는 번들로 간다. 그리는 쪽이 어떤 토큰을
|
|
155
|
-
* 풀 수 있는지 아는 유일한 자리이므로 그쪽이 보낸다.
|
|
156
|
-
*
|
|
157
|
-
* 돌아오는 것은 **후보**다. 저장되지 않았다 — 저작자가 받아야 정본이 된다.
|
|
158
|
-
*/
|
|
159
|
-
export async function proposeFigure(request) {
|
|
160
|
-
const response = await client.mutate({
|
|
161
|
-
mutation: gql `
|
|
162
|
-
mutation ProposeFigure($request: ProposeRequest!) {
|
|
163
|
-
proposeFigure(request: $request) {
|
|
164
|
-
source
|
|
165
|
-
score
|
|
166
|
-
grade
|
|
167
|
-
triangles
|
|
168
|
-
groups
|
|
169
|
-
quality {
|
|
170
|
-
status
|
|
171
|
-
summary
|
|
172
|
-
findings {
|
|
173
|
-
dimension
|
|
174
|
-
code
|
|
175
|
-
message
|
|
176
|
-
}
|
|
177
|
-
visualRegions
|
|
178
|
-
visualCoverage
|
|
179
|
-
visualReadability
|
|
180
|
-
}
|
|
181
|
-
attempts
|
|
182
|
-
violations {
|
|
183
|
-
code
|
|
184
|
-
message
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
`,
|
|
189
|
-
variables: { request },
|
|
190
|
-
// @operato/graphql 는 이 신호가 있을 때만 Upload가 포함된 변수를 multipart로 인코딩한다.
|
|
191
|
-
// 없으면 브라우저 File은 JSON에서 {}가 되어 서버 Upload scalar가 요청 자체를 거절한다.
|
|
192
|
-
context: { hasUpload: !!request.image }
|
|
193
|
-
});
|
|
194
|
-
return unwrap(response, 'proposeFigure');
|
|
195
|
-
}
|
|
196
150
|
/**
|
|
197
151
|
* 발행한다 — 판 번호가 오르고, 그 순간이 판본으로 남는다.
|
|
198
152
|
*
|