@things-factory/figure-ui 10.1.6 → 10.1.8
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/graphql/index.ts +25 -0
- package/client/modeller/figure-animations.ts +769 -0
- package/client/modeller/figure-ask.ts +6 -1
- package/client/modeller/figure-history-panel.ts +52 -1
- package/client/modeller/figure-inspector.ts +520 -120
- package/client/modeller/figure-side.ts +65 -17
- package/client/modeller/proposal-session.ts +92 -0
- package/client/pages/figure-list-page.ts +56 -4
- package/client/pages/figure-modeller-page.ts +204 -179
- package/client/types.ts +39 -0
- package/client/viewparts/figure-thumb.ts +78 -0
- package/dist-client/graphql/index.d.ts +3 -1
- package/dist-client/graphql/index.js +22 -0
- package/dist-client/graphql/index.js.map +1 -1
- package/dist-client/modeller/figure-animations.d.ts +96 -0
- package/dist-client/modeller/figure-animations.js +708 -0
- package/dist-client/modeller/figure-animations.js.map +1 -0
- package/dist-client/modeller/figure-ask.d.ts +3 -0
- package/dist-client/modeller/figure-ask.js +9 -0
- package/dist-client/modeller/figure-ask.js.map +1 -1
- package/dist-client/modeller/figure-history-panel.d.ts +7 -0
- package/dist-client/modeller/figure-history-panel.js +50 -0
- package/dist-client/modeller/figure-history-panel.js.map +1 -1
- package/dist-client/modeller/figure-inspector.d.ts +17 -0
- package/dist-client/modeller/figure-inspector.js +506 -107
- package/dist-client/modeller/figure-inspector.js.map +1 -1
- package/dist-client/modeller/figure-side.d.ts +23 -0
- package/dist-client/modeller/figure-side.js +68 -16
- package/dist-client/modeller/figure-side.js.map +1 -1
- package/dist-client/modeller/proposal-session.d.ts +23 -0
- package/dist-client/modeller/proposal-session.js +65 -0
- package/dist-client/modeller/proposal-session.js.map +1 -0
- package/dist-client/pages/figure-list-page.d.ts +10 -0
- package/dist-client/pages/figure-list-page.js +55 -4
- package/dist-client/pages/figure-list-page.js.map +1 -1
- package/dist-client/pages/figure-modeller-page.d.ts +50 -28
- package/dist-client/pages/figure-modeller-page.js +197 -176
- package/dist-client/pages/figure-modeller-page.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/types.d.ts +44 -0
- package/dist-client/types.js.map +1 -1
- package/dist-client/viewparts/figure-thumb.js +74 -0
- package/dist-client/viewparts/figure-thumb.js.map +1 -1
- package/package.json +4 -5
- package/test/ai-proposal-contract.test.ts +169 -0
- package/test/i18n-prefix-guard.test.ts +42 -0
- package/translations/en.json +82 -6
- package/translations/ja.json +34 -1
- package/translations/ko.json +92 -6
- package/translations/ms.json +34 -1
- package/translations/zh.json +34 -1
|
@@ -1,18 +1,6 @@
|
|
|
1
1
|
import '@material/web/icon/icon.js'
|
|
2
2
|
import '../modeller/figure-side.js'
|
|
3
3
|
import '../modeller/figure-ask.js'
|
|
4
|
-
/*
|
|
5
|
-
대화 도크. **`/client` 서브패스로 적으면 안 된다.**
|
|
6
|
-
|
|
7
|
-
그 패키지에 `exports` 맵이 없어서 `/client` 는 소스 디렉터리로 떨어지고, webpack 이 `.ts` 를 직접
|
|
8
|
-
컴파일한다. 그 경로로 나온 데코레이터가 `Reflect.metadata` 를 가드 없이 불러 **브라우저에서만**
|
|
9
|
-
죽는다(빌드 로그는 오류 0건). board-ai 와 plant 가 차례로 밟았다.
|
|
10
|
-
|
|
11
|
-
그리고 더 센 이유가 있다 — `board-ui/dist-client` 의 모델러가 같은 엘리먼트를 **빌드된 경로로**
|
|
12
|
-
등록한다. 다른 경로로 적으면 webpack 이 두 모듈로 보고, 두 곳이 같은 커스텀 엘리먼트 이름을
|
|
13
|
-
등록해 나중에 뜨는 화면이 죽는다. 즉 「어느 경로가 되나」가 아니라 **「모두 같은 경로여야 한다」**다.
|
|
14
|
-
*/
|
|
15
|
-
import '@things-factory/ai-assistant/dist-client/index.js'
|
|
16
4
|
import '../modeller/figure-canvas.js'
|
|
17
5
|
import { captureThumbnail } from '../modeller/figure-thumbnail.js'
|
|
18
6
|
import '../modeller/figure-preview.js'
|
|
@@ -25,17 +13,18 @@ import { customElement, state } from 'lit/decorators.js'
|
|
|
25
13
|
import { i18next, localize } from '@operato/i18n'
|
|
26
14
|
import { navigate, PageView } from '@operato/shell'
|
|
27
15
|
import { validate } from '@hatiolab/figure-model'
|
|
28
|
-
import { activePalette } from '@hatiolab/things-scene'
|
|
29
16
|
import type { DetailLevel, FigureSource, PrimitiveKind } from '@hatiolab/figure-model'
|
|
30
17
|
|
|
31
18
|
import * as edits from '../modeller/part-edits.js'
|
|
32
19
|
import * as proposals from '../modeller/proposal.js'
|
|
20
|
+
import * as proposalSessions from '../modeller/proposal-session.js'
|
|
21
|
+
import type { ProposalSession } from '../modeller/proposal-session.js'
|
|
33
22
|
import { toFigureSource, fromFigureSource } from '../modeller/figure-source.js'
|
|
34
23
|
import type { BoardModel, PartModel, PlaneAxis } from '../modeller/figure-source.js'
|
|
35
24
|
import { DEFAULT_VIEW } from '../modeller/figure-view.js'
|
|
36
25
|
import type { ViewSettings } from '../modeller/figure-view.js'
|
|
37
26
|
import { createFigure, fetchFigure, fetchFigureTypeNames, updateFigure } from '../graphql/index.js'
|
|
38
|
-
import type { Figure, FigureFinding } from '../types.js'
|
|
27
|
+
import type { Figure, FigureFinding, FigureGateFix, FigureProposal, ProposalFeedback } from '../types.js'
|
|
39
28
|
|
|
40
29
|
const FigureModellerPageBase = localize(i18next)(PageView) as typeof PageView
|
|
41
30
|
|
|
@@ -147,50 +136,10 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
147
136
|
div[edits] {
|
|
148
137
|
flex: 1;
|
|
149
138
|
display: grid;
|
|
150
|
-
grid-template-columns: 220px 1fr 320px
|
|
139
|
+
grid-template-columns: 220px 1fr 320px;
|
|
151
140
|
min-height: 0;
|
|
152
141
|
overflow: hidden;
|
|
153
142
|
}
|
|
154
|
-
/*
|
|
155
|
-
대화 도크는 **불렀을 때만 자리를 받는다.** 저작의 주인공은 캔버스이고, 대화는 필요할 때
|
|
156
|
-
여는 것이다. 늘 열어 두면 넷으로 나뉜 화면에서 캔버스가 가장 좁아진다.
|
|
157
|
-
*/
|
|
158
|
-
div[edits][dock] {
|
|
159
|
-
grid-template-columns: 220px 1fr 320px 360px;
|
|
160
|
-
}
|
|
161
|
-
div[edits] > ox-board-ai-chat {
|
|
162
|
-
grid-column: 4;
|
|
163
|
-
min-width: 0;
|
|
164
|
-
border-left: 1px solid var(--md-sys-color-outline-variant);
|
|
165
|
-
background-color: var(--md-sys-color-surface-container-lowest);
|
|
166
|
-
}
|
|
167
|
-
div[edits]:not([dock]) > ox-board-ai-chat {
|
|
168
|
-
display: none;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/* 도크를 여는 손잡이. 머리줄의 저장 단추 옆에 둔다 */
|
|
172
|
-
button[dock] {
|
|
173
|
-
flex: none;
|
|
174
|
-
display: flex;
|
|
175
|
-
align-items: center;
|
|
176
|
-
gap: 4px;
|
|
177
|
-
padding: 5px 10px;
|
|
178
|
-
font: var(--label-font, inherit);
|
|
179
|
-
font-size: 0.78rem;
|
|
180
|
-
color: var(--md-sys-color-on-surface-variant);
|
|
181
|
-
background: none;
|
|
182
|
-
border: 1px solid var(--md-sys-color-outline-variant);
|
|
183
|
-
border-radius: 6px;
|
|
184
|
-
cursor: pointer;
|
|
185
|
-
}
|
|
186
|
-
button[dock][on] {
|
|
187
|
-
color: var(--md-sys-color-primary);
|
|
188
|
-
border-color: var(--md-sys-color-primary);
|
|
189
|
-
background-color: var(--md-sys-color-surface-container);
|
|
190
|
-
}
|
|
191
|
-
button[dock] md-icon {
|
|
192
|
-
font-size: 16px;
|
|
193
|
-
}
|
|
194
143
|
|
|
195
144
|
/*
|
|
196
145
|
**세 칸의 자리를 못 박는다.**
|
|
@@ -489,6 +438,39 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
489
438
|
cursor: default;
|
|
490
439
|
}
|
|
491
440
|
|
|
441
|
+
/* AI의 평가는 판정문이 아니라 수락 전에 읽을 근거다. 숨기지 않고 변경 칩 위에 둔다. */
|
|
442
|
+
div[quality-review] {
|
|
443
|
+
padding: 7px var(--spacing-large, 12px);
|
|
444
|
+
background-color: var(--md-sys-color-primary-container);
|
|
445
|
+
border-bottom: 1px solid var(--md-sys-color-outline-variant);
|
|
446
|
+
color: var(--md-sys-color-on-primary-container);
|
|
447
|
+
font-size: 0.72rem;
|
|
448
|
+
line-height: 1.45;
|
|
449
|
+
}
|
|
450
|
+
div[quality-review][review] {
|
|
451
|
+
background-color: var(--md-sys-color-tertiary-container);
|
|
452
|
+
color: var(--md-sys-color-on-tertiary-container);
|
|
453
|
+
}
|
|
454
|
+
div[quality-review] strong { display: block; font-size: 0.74rem; }
|
|
455
|
+
div[quality-review] ul { margin: 4px 0 0; padding-left: 18px; }
|
|
456
|
+
div[proposal-note] {
|
|
457
|
+
padding: 6px var(--spacing-large, 12px) 8px;
|
|
458
|
+
border-bottom: 1px solid var(--md-sys-color-outline-variant);
|
|
459
|
+
background-color: var(--md-sys-color-surface-container-low);
|
|
460
|
+
}
|
|
461
|
+
div[proposal-note] label { display: block; font-size: 0.71rem; color: var(--md-sys-color-on-surface-variant); }
|
|
462
|
+
div[proposal-note] input {
|
|
463
|
+
box-sizing: border-box;
|
|
464
|
+
width: 100%;
|
|
465
|
+
margin-top: 4px;
|
|
466
|
+
padding: 5px 7px;
|
|
467
|
+
border: 1px solid var(--md-sys-color-outline-variant);
|
|
468
|
+
border-radius: 5px;
|
|
469
|
+
color: var(--md-sys-color-on-surface);
|
|
470
|
+
background: var(--md-sys-color-surface);
|
|
471
|
+
font: inherit;
|
|
472
|
+
}
|
|
473
|
+
|
|
492
474
|
/*
|
|
493
475
|
무엇을 바꾸자는 것인가 — 칩 하나가 변경 하나다.
|
|
494
476
|
|
|
@@ -584,14 +566,10 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
584
566
|
* 미저장 표시(`dirty`)는 그대로 남으므로 데이터를 잃지는 않았다. 잃은 것은 **사유**다.
|
|
585
567
|
*/
|
|
586
568
|
@state() private saveFailure = ''
|
|
587
|
-
/**
|
|
588
|
-
@state() private
|
|
589
|
-
/**
|
|
590
|
-
@state() private
|
|
591
|
-
/** 그 후보의 셈. 무엇을 받는 것인지 숫자로도 보여 준다. */
|
|
592
|
-
@state() private proposalOf?: { grade: string; triangles: number; groups: number; attempts: number }
|
|
593
|
-
/** 받기로 고른 변경들. 처음에는 전부 골라 둔다. */
|
|
594
|
-
@state() private picked = new Set<string>()
|
|
569
|
+
/** 저작 보조 또는 발행 게이트가 연 후보 검토 세션. 받기 전에는 정본이 아니다. */
|
|
570
|
+
@state() private proposalSession?: ProposalSession
|
|
571
|
+
/** 인라인 요청창과 AI 도크가 함께 쓰는 현재 Figure의 제한된 세션 피드백. */
|
|
572
|
+
@state() private proposalFeedback: ProposalFeedback[] = []
|
|
595
573
|
|
|
596
574
|
/**
|
|
597
575
|
* 저장 형식으로 변환한 것. 씬 모델이 바뀔 때만 다시 만든다.
|
|
@@ -607,6 +585,35 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
607
585
|
}
|
|
608
586
|
}
|
|
609
587
|
|
|
588
|
+
/**
|
|
589
|
+
* 페이지 밖 AI 도크에도 **지금 손에 든 정본**을 알려 준다.
|
|
590
|
+
*
|
|
591
|
+
* 모델러 안의 `figure-ask` 는 이미 `folded`를 직접 받는다. 반면 asidebar 도크는 주소의 id만
|
|
592
|
+
* 알았으므로 DB에 저장된 옛 판을 읽었다. 저장 전 변경을 두고 "왜 발행이 안 되나" 또는
|
|
593
|
+
* "이 부품을 줄여 달라"고 물으면 대화가 엉뚱한 판을 보게 되는 결함이었다.
|
|
594
|
+
*
|
|
595
|
+
* 이 이벤트는 저장하지 않는다. 도크가 대화 인자로만 잠시 들고 있고, 서버 도구도 도메인 소속
|
|
596
|
+
* Figure를 먼저 확인한 뒤에만 이 source를 사용한다. 페이지를 떠날 때는 아래에서 지워 오래된
|
|
597
|
+
* 초안이 다음 Figure에 섞이지 않게 한다.
|
|
598
|
+
*/
|
|
599
|
+
updated(changed: Map<string, unknown>) {
|
|
600
|
+
if (changed.has('figure') && (changed.get('figure') as Figure | undefined)?.id !== this.figure?.id) {
|
|
601
|
+
this.proposalFeedback = []
|
|
602
|
+
}
|
|
603
|
+
if (changed.has('board') || changed.has('parts') || changed.has('figure')) {
|
|
604
|
+
window.dispatchEvent(
|
|
605
|
+
new CustomEvent('figure-ai-context', {
|
|
606
|
+
detail: { figureId: this.figure?.id, source: this.folded }
|
|
607
|
+
})
|
|
608
|
+
)
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
disconnectedCallback() {
|
|
613
|
+
window.dispatchEvent(new CustomEvent('figure-ai-context', { detail: undefined }))
|
|
614
|
+
super.disconnectedCallback()
|
|
615
|
+
}
|
|
616
|
+
|
|
610
617
|
get context() {
|
|
611
618
|
return {
|
|
612
619
|
title: this.figure?.name ?? i18next.t('figure.title.new-figure'),
|
|
@@ -642,17 +649,10 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
642
649
|
<figure-ask
|
|
643
650
|
.source=${source}
|
|
644
651
|
.type=${source?.type ?? ''}
|
|
652
|
+
.feedback=${this.proposalFeedback}
|
|
645
653
|
@proposed=${(e: CustomEvent) => this.receive(e.detail.proposal)}
|
|
646
654
|
></figure-ask>
|
|
647
655
|
${this.saveFailure ? html`<span save-failed title=${this.saveFailure}>${this.saveFailure}</span>` : ''}
|
|
648
|
-
<button
|
|
649
|
-
dock
|
|
650
|
-
?on=${this.dockOpen}
|
|
651
|
-
title=${i18next.t('figure.text.dock-explained')}
|
|
652
|
-
@click=${() => (this.dockOpen = !this.dockOpen)}
|
|
653
|
-
>
|
|
654
|
-
<md-icon>forum</md-icon>${i18next.t('figure.button.dock')}
|
|
655
|
-
</button>
|
|
656
656
|
<button save ?disabled=${!this.canSave} @click=${() => this.save()}>
|
|
657
657
|
${this.saving ? i18next.t('figure.text.saving-figure') : i18next.t('figure.button.save')}
|
|
658
658
|
</button>
|
|
@@ -662,10 +662,12 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
662
662
|
edits
|
|
663
663
|
?collapse-left=${this.collapsed.left}
|
|
664
664
|
?collapse-right=${this.collapsed.right}
|
|
665
|
-
?dock=${this.dockOpen}
|
|
666
665
|
@figure-rename=${(e: CustomEvent) => this.rename(e.detail.name)}
|
|
666
|
+
@figure-catalog=${(e: CustomEvent) => this.catalog(e.detail)}
|
|
667
667
|
@figure-released=${(e: CustomEvent) => this.released(e.detail.figure)}
|
|
668
668
|
@figure-reverted=${() => this.figure?.id && this.load(this.figure.id)}
|
|
669
|
+
@preview-sizing-fix=${(e: CustomEvent) => this.previewSizingFix(e.detail)}
|
|
670
|
+
@apply-sizing-fix=${(e: CustomEvent) => this.applySizingFix(e.detail)}
|
|
669
671
|
@add-part=${(e: CustomEvent) => this.addPart(e.detail.primitive)}
|
|
670
672
|
@select-part=${(e: CustomEvent) => (this.selected = e.detail.index)}
|
|
671
673
|
@remove-part=${(e: CustomEvent) => this.removePart(e.detail.index)}
|
|
@@ -689,8 +691,8 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
689
691
|
|
|
690
692
|
<div middle>
|
|
691
693
|
${this.renderBar()}
|
|
692
|
-
${this.
|
|
693
|
-
${this.
|
|
694
|
+
${this.proposalSession ? this.renderDecision() : ''}
|
|
695
|
+
${this.proposalSession
|
|
694
696
|
? html`
|
|
695
697
|
<div lanes>
|
|
696
698
|
<div lane>
|
|
@@ -705,10 +707,11 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
705
707
|
<div lane candidate>
|
|
706
708
|
<h4>
|
|
707
709
|
${i18next.t('figure.label.proposed')}
|
|
708
|
-
${this.
|
|
710
|
+
${this.proposalSession.metrics
|
|
709
711
|
? html`<small>
|
|
710
|
-
${this.
|
|
711
|
-
${i18next.t('figure.text.n-triangles', { count: this.
|
|
712
|
+
${this.proposalSession.metrics.grade} ·
|
|
713
|
+
${i18next.t('figure.text.n-triangles', { count: this.proposalSession.metrics.triangles })}
|
|
714
|
+
${this.proposalSession.metrics.quality ? html` · ${this.proposalSession.metrics.quality.summary}` : ''}
|
|
712
715
|
</small>`
|
|
713
716
|
: ''}
|
|
714
717
|
</h4>
|
|
@@ -727,6 +730,8 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
727
730
|
|
|
728
731
|
<figure-side
|
|
729
732
|
.figureName=${this.figure?.name ?? ''}
|
|
733
|
+
.figureCategory=${this.figure?.category}
|
|
734
|
+
.figureTags=${this.figure?.tags}
|
|
730
735
|
.figureId=${this.figure?.id ?? ''}
|
|
731
736
|
.figureState=${this.figure?.state}
|
|
732
737
|
.figureVersion=${this.figure?.version ?? 0}
|
|
@@ -738,71 +743,10 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
738
743
|
.view=${this.view}
|
|
739
744
|
@update-view=${(e: CustomEvent) => (this.view = e.detail.view)}
|
|
740
745
|
></figure-side>
|
|
741
|
-
|
|
742
|
-
<!-- 대화 도크. 무엇을 켜고 끄는지는 renderDock 위에 적어 두었다 -->
|
|
743
|
-
<ox-board-ai-chat
|
|
744
|
-
.boardTools=${false}
|
|
745
|
-
.toolCategories=${['figure-authoring']}
|
|
746
|
-
.hostContext=${this.hostContext}
|
|
747
|
-
.placeholder=${i18next.t('figure.text.dock-placeholder')}
|
|
748
|
-
@board-ai-proposal-execute=${(e: CustomEvent) => this.takeProposal(e.detail.proposal)}
|
|
749
|
-
></ox-board-ai-chat>
|
|
750
746
|
</div>
|
|
751
747
|
`
|
|
752
748
|
}
|
|
753
749
|
|
|
754
|
-
/*
|
|
755
|
-
* ── 대화 도크를 무엇으로 켜나 ───────────────────────────────────────────────
|
|
756
|
-
*
|
|
757
|
-
* board 의 컴포넌트를 그대로 쓰지만 **board 인 척하지 않는다.** `boardTools` 를 끄고 figure 의
|
|
758
|
-
* 도구 범주(`figure-authoring`)만 연다. 끄지 않으면 보드 편집 도구가 함께 들려, 모델이 이
|
|
759
|
-
* 화면에서 할 수 없는 일을 시도한다.
|
|
760
|
-
*
|
|
761
|
-
* board 모양의 인자(`currentBoard` · `knownTypes` · `componentSchemas` · `selectedRefids`)는
|
|
762
|
-
* 주지 않는다 — 전부 선택이고, 채우려면 없는 것을 지어내야 한다.
|
|
763
|
-
*/
|
|
764
|
-
|
|
765
|
-
/**
|
|
766
|
-
* 어시스턴트가 「지금 무엇을 열고 있나」를 아는 길.
|
|
767
|
-
*
|
|
768
|
-
* 도구가 대상을 안 받았을 때 이 문맥에서 해소한다 — 사용자에게 식별자를 되묻지 않는다.
|
|
769
|
-
* 팔레트도 함께 보낸다: 모르면 모델이 색을 지어내고, 그 후보는 검증을 지나 **그리는 시점에**
|
|
770
|
-
* 죽는다.
|
|
771
|
-
*/
|
|
772
|
-
private get hostContext() {
|
|
773
|
-
return {
|
|
774
|
-
surface: 'figure-modeller',
|
|
775
|
-
figureId: this.figure?.id || undefined,
|
|
776
|
-
figureType: this.board?.figureType,
|
|
777
|
-
figureName: this.figure?.name,
|
|
778
|
-
palette: Object.keys(activePalette() as Record<string, string>)
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
/**
|
|
783
|
-
* 도크의 후보를 받는다.
|
|
784
|
-
*
|
|
785
|
-
* **후보가 화면에 닿는 길은 하나뿐이다** — 말로 시키는 줄이든 도크든 `receive()` 로 모인다.
|
|
786
|
-
* 두 길을 두면 「지금 / 후보」 두 칸을 두 곳에서 그리게 되고, 어느 쪽이 이기는지 저작자가
|
|
787
|
-
* 매번 생각해야 한다.
|
|
788
|
-
*/
|
|
789
|
-
private takeProposal(proposal: any) {
|
|
790
|
-
const source = proposal?.source
|
|
791
|
-
if (!source) {
|
|
792
|
-
// 카드는 왔는데 후보가 없다 — 조용히 넘기지 않는다. 저작자가 눌렀으니 답이 있어야 한다.
|
|
793
|
-
this.saveFailure = i18next.t('figure.text.proposal-had-no-source')
|
|
794
|
-
return
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
this.receive({
|
|
798
|
-
source: JSON.stringify(source),
|
|
799
|
-
grade: proposal.score?.grade ?? '',
|
|
800
|
-
triangles: proposal.cost?.triangles ?? 0,
|
|
801
|
-
groups: proposal.cost?.groups ?? 0,
|
|
802
|
-
attempts: proposal.attempts ?? 1
|
|
803
|
-
})
|
|
804
|
-
}
|
|
805
|
-
|
|
806
750
|
/**
|
|
807
751
|
* 가운데 칸 위의 띠 — 무엇을 보고 있나와, 양쪽 칸을 접는 손잡이.
|
|
808
752
|
*
|
|
@@ -833,7 +777,7 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
833
777
|
`
|
|
834
778
|
|
|
835
779
|
return html`<div modes>
|
|
836
|
-
${this.renderCollapse('left')}${this.
|
|
780
|
+
${this.renderCollapse('left')}${this.proposalSession ? '' : html`${one('edit')}${one('preview')}`}
|
|
837
781
|
<div spacer></div>
|
|
838
782
|
${this.renderCollapse('right')}
|
|
839
783
|
</div>`
|
|
@@ -868,16 +812,17 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
868
812
|
}
|
|
869
813
|
|
|
870
814
|
/**
|
|
871
|
-
* AI 후보를
|
|
815
|
+
* AI 후보를 실제 배치 크기에서도 본다.
|
|
872
816
|
*
|
|
873
|
-
*
|
|
874
|
-
*
|
|
817
|
+
* 편집 캔버스 한 장으로는 `repeat`·`fixed`·`anchor`가 맞는지 알 수 없다. 후보는 아직
|
|
818
|
+
* 정본이 아니므로 저장하지 않고, `figure-preview`가 같은 FigureSource를 기준 크기와
|
|
819
|
+
* 일곱 가지 확장 축에서 실제 FigureInstance로 세운다. 따라서 저작자는 "좋아 보이는
|
|
820
|
+
* 한 장"이 아니라 배치될 때도 붙어 있는지를 보고 수락한다.
|
|
875
821
|
*/
|
|
876
822
|
private renderProposed() {
|
|
877
|
-
if (!this.
|
|
823
|
+
if (!this.proposalSession) return ''
|
|
878
824
|
|
|
879
|
-
|
|
880
|
-
return html`<figure-canvas .board=${board} .parts=${parts} .viewOnly=${true}></figure-canvas>`
|
|
825
|
+
return html`<figure-preview .source=${this.proposalSession.source}></figure-preview>`
|
|
881
826
|
}
|
|
882
827
|
|
|
883
828
|
/**
|
|
@@ -895,22 +840,35 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
895
840
|
* 안 들고, 우리가 생각 못 한 조합을 막아 주기 때문이다.
|
|
896
841
|
*/
|
|
897
842
|
private renderDecision() {
|
|
898
|
-
if (!this.
|
|
843
|
+
if (!this.proposalSession) return ''
|
|
899
844
|
|
|
900
|
-
const changes = proposals.diffProposal(this.folded, this.
|
|
901
|
-
const picked = proposals.applyProposal(this.folded, this.
|
|
845
|
+
const changes = proposals.diffProposal(this.folded, this.proposalSession.source)
|
|
846
|
+
const picked = proposals.applyProposal(this.folded, this.proposalSession.source, this.proposalSession.picked)
|
|
902
847
|
const { errors, violations } = validate(picked)
|
|
903
848
|
|
|
904
849
|
return html`
|
|
905
850
|
<div decide>
|
|
906
|
-
<span>${i18next.t('figure.text.assistant-proposed-a-figure')}</span>
|
|
851
|
+
<span>${this.proposalSession.title || i18next.t('figure.text.assistant-proposed-a-figure')}</span>
|
|
907
852
|
<div spacer></div>
|
|
908
853
|
<button drop @click=${() => this.discard()}>${i18next.t('figure.button.discard')}</button>
|
|
909
|
-
<button take ?disabled=${errors.length > 0 || this.picked.size === 0} @click=${() => this.take()}>
|
|
910
|
-
${i18next.t('figure.button.take-n-changes', { n: this.picked.size })}
|
|
854
|
+
<button take ?disabled=${errors.length > 0 || this.proposalSession.picked.size === 0} @click=${() => this.take()}>
|
|
855
|
+
${i18next.t('figure.button.take-n-changes', { n: this.proposalSession.picked.size })}
|
|
911
856
|
</button>
|
|
912
857
|
</div>
|
|
913
858
|
|
|
859
|
+
${this.renderQualityReview()}
|
|
860
|
+
<div proposal-note>
|
|
861
|
+
<label>
|
|
862
|
+
${i18next.t('figure.text.proposal-feedback-note')}
|
|
863
|
+
<input
|
|
864
|
+
maxlength="500"
|
|
865
|
+
.value=${this.proposalSession.note}
|
|
866
|
+
placeholder=${i18next.t('figure.text.proposal-feedback-placeholder')}
|
|
867
|
+
@input=${(event: Event) => (this.proposalSession = { ...this.proposalSession!, note: (event.target as HTMLInputElement).value })}
|
|
868
|
+
/>
|
|
869
|
+
</label>
|
|
870
|
+
</div>
|
|
871
|
+
|
|
914
872
|
${changes.length > 0
|
|
915
873
|
? html`
|
|
916
874
|
<div changes>
|
|
@@ -926,6 +884,25 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
926
884
|
`
|
|
927
885
|
}
|
|
928
886
|
|
|
887
|
+
/** 후보 품질 판단의 근거를 수락 전에 전부 보여 준다. */
|
|
888
|
+
private renderQualityReview() {
|
|
889
|
+
const quality = this.proposalSession?.metrics?.quality
|
|
890
|
+
if (!quality) return ''
|
|
891
|
+
|
|
892
|
+
return html`
|
|
893
|
+
<div quality-review ?review=${quality.status === 'review'}>
|
|
894
|
+
<strong>${i18next.t('figure.label.candidate-review')}</strong>
|
|
895
|
+
<span>${quality.summary}</span>
|
|
896
|
+
<span> · top-view ${Math.round(quality.visualReadability * 100)}% · ${quality.visualRegions} regions</span>
|
|
897
|
+
${quality.findings.length > 0
|
|
898
|
+
? html`<ul>
|
|
899
|
+
${quality.findings.map(finding => html`<li><code>${finding.dimension}</code> — ${finding.message}</li>`)}
|
|
900
|
+
</ul>`
|
|
901
|
+
: ''}
|
|
902
|
+
</div>
|
|
903
|
+
`
|
|
904
|
+
}
|
|
905
|
+
|
|
929
906
|
/** 변경 하나. 무엇을 하겠다는 것인지 부호로 먼저 말한다. */
|
|
930
907
|
private renderChange(change: proposals.ProposalChange) {
|
|
931
908
|
const icon = {
|
|
@@ -938,7 +915,7 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
938
915
|
styleKit: 'palette'
|
|
939
916
|
}[change.kind]
|
|
940
917
|
|
|
941
|
-
const on = this.picked.has(change.key)
|
|
918
|
+
const on = this.proposalSession?.picked.has(change.key)
|
|
942
919
|
|
|
943
920
|
return html`
|
|
944
921
|
<button
|
|
@@ -954,10 +931,7 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
954
931
|
}
|
|
955
932
|
|
|
956
933
|
private toggle(key: string) {
|
|
957
|
-
|
|
958
|
-
if (next.has(key)) next.delete(key)
|
|
959
|
-
else next.add(key)
|
|
960
|
-
this.picked = next
|
|
934
|
+
if (this.proposalSession) this.proposalSession = proposalSessions.toggleProposalChange(this.proposalSession, key)
|
|
961
935
|
}
|
|
962
936
|
|
|
963
937
|
/**
|
|
@@ -966,22 +940,29 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
966
940
|
* 원본 데이터에 넣지 않는다. 나란히 두고 저작자가 고른다. AI 가 원본 데이터에 직접 쓰면
|
|
967
941
|
* 무엇이 자기 것이고 무엇이 기계 것인지 모르게 된다.
|
|
968
942
|
*/
|
|
969
|
-
private receive(proposal: {
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
this.
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
943
|
+
private receive(proposal: FigureProposal) {
|
|
944
|
+
this.proposalSession = proposalSessions.openAiProposal(this.folded, proposal)
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
/**
|
|
948
|
+
* 발행 게이트의 구조화된 수정안 하나를 현재 작업 사본에만 겹쳐 본다. 서버가 준 값은
|
|
949
|
+
* `part.anchor.axis = to`라는 데이터이지 사람이 읽을 문장을 다시 해석한 결과가 아니다.
|
|
950
|
+
* 원본 `board`/`parts`는 여기서 전혀 바꾸지 않고, 아래의 `take()`만 바꾼 사본을 적용한다.
|
|
951
|
+
*/
|
|
952
|
+
private previewSizingFix(fix: FigureGateFix) {
|
|
953
|
+
if (!this.folded) return
|
|
954
|
+
this.proposalSession = proposalSessions.openSizingFix(this.folded, fix)
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
/**
|
|
958
|
+
* 원클릭 적용은 서버가 모든 크기 변경 case를 재검사해 안전하다고 표시한 한-앵커 후보에만 연다.
|
|
959
|
+
* 그래도 저장은 저작자의 별도 행위다. 잘못 눌러도 DB 정본이나 발행판은 바뀌지 않는다.
|
|
960
|
+
*/
|
|
961
|
+
private applySizingFix(fix: FigureGateFix) {
|
|
962
|
+
if (!fix.safe || !this.folded) return
|
|
963
|
+
this.previewSizingFix(fix)
|
|
964
|
+
if (!this.proposalSession) return
|
|
965
|
+
this.take()
|
|
985
966
|
}
|
|
986
967
|
|
|
987
968
|
/**
|
|
@@ -991,22 +972,45 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
991
972
|
* 하므로 그쪽이 자연스럽다. 받아들인 결과는 다시 씬 모델로 되돌려 편집을 이어간다.
|
|
992
973
|
*/
|
|
993
974
|
private take() {
|
|
994
|
-
if (!this.
|
|
975
|
+
if (!this.proposalSession) return
|
|
995
976
|
|
|
996
|
-
const taken = proposals.applyProposal(this.folded, this.
|
|
977
|
+
const taken = proposals.applyProposal(this.folded, this.proposalSession.source, this.proposalSession.picked)
|
|
997
978
|
const { board, parts } = fromFigureSource(taken)
|
|
998
979
|
|
|
999
980
|
this.board = board
|
|
1000
981
|
this.parts = parts
|
|
1001
982
|
this.selected = parts.length ? 0 : -1
|
|
1002
983
|
this.dirty = true
|
|
1003
|
-
this.
|
|
984
|
+
this.reportProposalFeedback('accepted')
|
|
985
|
+
this.discard(false)
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
/**
|
|
989
|
+
* 후보에 대한 사람의 결정을 도크에만 알린다.
|
|
990
|
+
*
|
|
991
|
+
* 이 기록은 분석 이벤트나 Figure 속성이 아니다. 같은 모델러 세션에서 다음 요청을 더 잘
|
|
992
|
+
* 이해시키는 짧은 신호일 뿐이다. 원본 source나 사람의 작업 내용을 다시 보내지 않고, 사람이
|
|
993
|
+
* 후보 변경 중 몇 개를 받아들였는지와 이미 화면에 보였던 품질 판정만 보낸다.
|
|
994
|
+
*/
|
|
995
|
+
private reportProposalFeedback(outcome: 'accepted' | 'discarded') {
|
|
996
|
+
if (!this.proposalSession) return
|
|
997
|
+
|
|
998
|
+
const feedback = proposalSessions.feedbackFromSession(this.folded, this.proposalSession, outcome)
|
|
999
|
+
if (!feedback) return
|
|
1000
|
+
this.proposalFeedback = [...this.proposalFeedback, feedback].slice(-5)
|
|
1001
|
+
window.dispatchEvent(
|
|
1002
|
+
new CustomEvent('figure-ai-feedback', {
|
|
1003
|
+
detail: {
|
|
1004
|
+
figureId: this.figure?.id,
|
|
1005
|
+
...feedback
|
|
1006
|
+
}
|
|
1007
|
+
})
|
|
1008
|
+
)
|
|
1004
1009
|
}
|
|
1005
1010
|
|
|
1006
|
-
private discard() {
|
|
1007
|
-
this.
|
|
1008
|
-
this.
|
|
1009
|
-
this.picked = new Set()
|
|
1011
|
+
private discard(reportFeedback = true) {
|
|
1012
|
+
if (reportFeedback) this.reportProposalFeedback('discarded')
|
|
1013
|
+
this.proposalSession = undefined
|
|
1010
1014
|
}
|
|
1011
1015
|
|
|
1012
1016
|
/**
|
|
@@ -1135,6 +1139,19 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
1135
1139
|
this.dirty = true
|
|
1136
1140
|
}
|
|
1137
1141
|
|
|
1142
|
+
/**
|
|
1143
|
+
* 분류와 태그를 고친다.
|
|
1144
|
+
*
|
|
1145
|
+
* 정본(`source`)이 아니라 **카탈로그의 값**이다 — 도형이 무엇으로 생겼는지가 아니라 어디에
|
|
1146
|
+
* 걸리는지다. 그래서 점수도 발행도 흔들리지 않는다.
|
|
1147
|
+
*/
|
|
1148
|
+
private catalog(patch: { category?: string; tags?: string[] }) {
|
|
1149
|
+
if (!this.figure) return
|
|
1150
|
+
|
|
1151
|
+
this.figure = { ...this.figure, ...patch }
|
|
1152
|
+
this.dirty = true
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1138
1155
|
/**
|
|
1139
1156
|
* 부품 목록을 갈아 끼운다.
|
|
1140
1157
|
*
|
|
@@ -1339,6 +1356,12 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
1339
1356
|
if (this.figure.id) {
|
|
1340
1357
|
const result = await updateFigure(this.figure.id, {
|
|
1341
1358
|
name: this.figure.name,
|
|
1359
|
+
/*
|
|
1360
|
+
카탈로그의 값도 함께 보낸다. 화면에서 고쳐 놓고 저장에 안 실으면 저장 뒤에 조용히
|
|
1361
|
+
사라진다 — 저작자는 저장했다고 보고 다시 열면 없다.
|
|
1362
|
+
*/
|
|
1363
|
+
category: this.figure.category,
|
|
1364
|
+
tags: this.figure.tags,
|
|
1342
1365
|
source: sourceJson,
|
|
1343
1366
|
thumbnail
|
|
1344
1367
|
})
|
|
@@ -1355,6 +1378,8 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
1355
1378
|
const result = await createFigure({
|
|
1356
1379
|
type: this.folded.type,
|
|
1357
1380
|
name: this.figure.name,
|
|
1381
|
+
category: this.figure.category,
|
|
1382
|
+
tags: this.figure.tags,
|
|
1358
1383
|
source: sourceJson,
|
|
1359
1384
|
thumbnail
|
|
1360
1385
|
})
|
package/client/types.ts
CHANGED
|
@@ -14,7 +14,10 @@ export interface Figure {
|
|
|
14
14
|
type: string
|
|
15
15
|
name: string
|
|
16
16
|
description?: string
|
|
17
|
+
/** 분류 하나 — 팔레트에서 묶어 보이는 기준. */
|
|
17
18
|
category?: string
|
|
19
|
+
/** 태그 여럿 — 분류로는 못 하는 찾기. */
|
|
20
|
+
tags?: string[]
|
|
18
21
|
state?: 'draft' | 'released'
|
|
19
22
|
/** 재활용 점수 0~100. 서버가 정본에서 계산한다 — 화면이 보내는 값이 아니다. */
|
|
20
23
|
score?: number
|
|
@@ -63,6 +66,7 @@ export interface NewFigure {
|
|
|
63
66
|
name: string
|
|
64
67
|
description?: string
|
|
65
68
|
category?: string
|
|
69
|
+
tags?: string[]
|
|
66
70
|
source: string
|
|
67
71
|
properties?: string
|
|
68
72
|
state?: 'draft' | 'released'
|
|
@@ -79,6 +83,8 @@ export interface FigurePatch {
|
|
|
79
83
|
name?: string
|
|
80
84
|
description?: string
|
|
81
85
|
category?: string
|
|
86
|
+
/** 빈 배열은 「전부 지운다」, 안 주는 것은 「건드리지 않는다」. */
|
|
87
|
+
tags?: string[]
|
|
82
88
|
source?: string
|
|
83
89
|
properties?: string
|
|
84
90
|
state?: 'draft' | 'released'
|
|
@@ -106,12 +112,31 @@ export interface FigureProposal {
|
|
|
106
112
|
triangles: number
|
|
107
113
|
/** 재질 묶음 = 인스턴스 하나가 무는 draw call. */
|
|
108
114
|
groups: number
|
|
115
|
+
/** Rule-based review of measurable design concerns; it is not an aesthetic verdict. */
|
|
116
|
+
quality: {
|
|
117
|
+
status: 'ready' | 'review'
|
|
118
|
+
summary: string
|
|
119
|
+
findings: Array<{ dimension: string; code: string; message: string }>
|
|
120
|
+
visualRegions: number
|
|
121
|
+
visualCoverage: number
|
|
122
|
+
visualReadability: number
|
|
123
|
+
}
|
|
109
124
|
/** 형식은 맞으나 정책을 넘은 것. 막지 않는다 — 저작자가 정한다. */
|
|
110
125
|
violations: FigureFinding[]
|
|
111
126
|
/** 몇 번 만에 됐나. 진단용이다. */
|
|
112
127
|
attempts: number
|
|
113
128
|
}
|
|
114
129
|
|
|
130
|
+
/** 사람이 후보를 수락하거나 버릴 때 직접 남긴 현재 저작 세션의 참고 신호. */
|
|
131
|
+
export interface ProposalFeedback {
|
|
132
|
+
outcome: 'accepted' | 'discarded'
|
|
133
|
+
selectedChanges: number
|
|
134
|
+
totalChanges: number
|
|
135
|
+
note?: string
|
|
136
|
+
grade?: string
|
|
137
|
+
quality?: { status?: 'ready' | 'review'; findingCodes?: string[] }
|
|
138
|
+
}
|
|
139
|
+
|
|
115
140
|
/**
|
|
116
141
|
* 발행 판정 한 줄.
|
|
117
142
|
*
|
|
@@ -119,6 +144,18 @@ export interface FigureProposal {
|
|
|
119
144
|
* 발행 판정은 막는 것과 알리는 것을 함께 내므로 어느 쪽인지를 값이 스스로 말한다 — 코드를 보고
|
|
120
145
|
* 화면이 다시 분류하면 규칙이 두 벌이 된다.
|
|
121
146
|
*/
|
|
147
|
+
/** One reversible anchor edit the sizing gate has measured against this exact source. */
|
|
148
|
+
export interface FigureGateFix {
|
|
149
|
+
findingWay: string
|
|
150
|
+
part: string
|
|
151
|
+
axis: 'x' | 'y' | 'z'
|
|
152
|
+
from?: string
|
|
153
|
+
to: 'min' | 'center' | 'max' | 'span'
|
|
154
|
+
resolved: number
|
|
155
|
+
/** All blocking sizing cases clear after this exact edit was remeasured server-side. */
|
|
156
|
+
safe: boolean
|
|
157
|
+
}
|
|
158
|
+
|
|
122
159
|
export interface FigureGateFinding {
|
|
123
160
|
code: string
|
|
124
161
|
message: string
|
|
@@ -128,6 +165,8 @@ export interface FigureGateFinding {
|
|
|
128
165
|
how?: string
|
|
129
166
|
/** 갈라진 부품 이름들, 또는 정본 안의 경로. */
|
|
130
167
|
at?: string
|
|
168
|
+
/** Candidate edits; the UI must preview and explicitly apply these, never auto-save them. */
|
|
169
|
+
fixes?: FigureGateFix[]
|
|
131
170
|
blocking: boolean
|
|
132
171
|
}
|
|
133
172
|
|