@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
|
@@ -2,18 +2,6 @@ import { __decorate, __metadata } from "tslib";
|
|
|
2
2
|
import '@material/web/icon/icon.js';
|
|
3
3
|
import '../modeller/figure-side.js';
|
|
4
4
|
import '../modeller/figure-ask.js';
|
|
5
|
-
/*
|
|
6
|
-
대화 도크. **`/client` 서브패스로 적으면 안 된다.**
|
|
7
|
-
|
|
8
|
-
그 패키지에 `exports` 맵이 없어서 `/client` 는 소스 디렉터리로 떨어지고, webpack 이 `.ts` 를 직접
|
|
9
|
-
컴파일한다. 그 경로로 나온 데코레이터가 `Reflect.metadata` 를 가드 없이 불러 **브라우저에서만**
|
|
10
|
-
죽는다(빌드 로그는 오류 0건). board-ai 와 plant 가 차례로 밟았다.
|
|
11
|
-
|
|
12
|
-
그리고 더 센 이유가 있다 — `board-ui/dist-client` 의 모델러가 같은 엘리먼트를 **빌드된 경로로**
|
|
13
|
-
등록한다. 다른 경로로 적으면 webpack 이 두 모듈로 보고, 두 곳이 같은 커스텀 엘리먼트 이름을
|
|
14
|
-
등록해 나중에 뜨는 화면이 죽는다. 즉 「어느 경로가 되나」가 아니라 **「모두 같은 경로여야 한다」**다.
|
|
15
|
-
*/
|
|
16
|
-
import '@things-factory/ai-assistant/dist-client/index.js';
|
|
17
5
|
import '../modeller/figure-canvas.js';
|
|
18
6
|
import { captureThumbnail } from '../modeller/figure-thumbnail.js';
|
|
19
7
|
import '../modeller/figure-preview.js';
|
|
@@ -24,9 +12,9 @@ import { customElement, state } from 'lit/decorators.js';
|
|
|
24
12
|
import { i18next, localize } from '@operato/i18n';
|
|
25
13
|
import { navigate, PageView } from '@operato/shell';
|
|
26
14
|
import { validate } from '@hatiolab/figure-model';
|
|
27
|
-
import { activePalette } from '@hatiolab/things-scene';
|
|
28
15
|
import * as edits from '../modeller/part-edits.js';
|
|
29
16
|
import * as proposals from '../modeller/proposal.js';
|
|
17
|
+
import * as proposalSessions from '../modeller/proposal-session.js';
|
|
30
18
|
import { toFigureSource, fromFigureSource } from '../modeller/figure-source.js';
|
|
31
19
|
import { DEFAULT_VIEW } from '../modeller/figure-view.js';
|
|
32
20
|
import { createFigure, fetchFigure, fetchFigureTypeNames, updateFigure } from '../graphql/index.js';
|
|
@@ -139,10 +127,8 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
139
127
|
* 미저장 표시(`dirty`)는 그대로 남으므로 데이터를 잃지는 않았다. 잃은 것은 **사유**다.
|
|
140
128
|
*/
|
|
141
129
|
this.saveFailure = '';
|
|
142
|
-
/**
|
|
143
|
-
this.
|
|
144
|
-
/** 받기로 고른 변경들. 처음에는 전부 골라 둔다. */
|
|
145
|
-
this.picked = new Set();
|
|
130
|
+
/** 인라인 요청창과 AI 도크가 함께 쓰는 현재 Figure의 제한된 세션 피드백. */
|
|
131
|
+
this.proposalFeedback = [];
|
|
146
132
|
}
|
|
147
133
|
static { this.styles = css `
|
|
148
134
|
:host {
|
|
@@ -166,50 +152,10 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
166
152
|
div[edits] {
|
|
167
153
|
flex: 1;
|
|
168
154
|
display: grid;
|
|
169
|
-
grid-template-columns: 220px 1fr 320px
|
|
155
|
+
grid-template-columns: 220px 1fr 320px;
|
|
170
156
|
min-height: 0;
|
|
171
157
|
overflow: hidden;
|
|
172
158
|
}
|
|
173
|
-
/*
|
|
174
|
-
대화 도크는 **불렀을 때만 자리를 받는다.** 저작의 주인공은 캔버스이고, 대화는 필요할 때
|
|
175
|
-
여는 것이다. 늘 열어 두면 넷으로 나뉜 화면에서 캔버스가 가장 좁아진다.
|
|
176
|
-
*/
|
|
177
|
-
div[edits][dock] {
|
|
178
|
-
grid-template-columns: 220px 1fr 320px 360px;
|
|
179
|
-
}
|
|
180
|
-
div[edits] > ox-board-ai-chat {
|
|
181
|
-
grid-column: 4;
|
|
182
|
-
min-width: 0;
|
|
183
|
-
border-left: 1px solid var(--md-sys-color-outline-variant);
|
|
184
|
-
background-color: var(--md-sys-color-surface-container-lowest);
|
|
185
|
-
}
|
|
186
|
-
div[edits]:not([dock]) > ox-board-ai-chat {
|
|
187
|
-
display: none;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/* 도크를 여는 손잡이. 머리줄의 저장 단추 옆에 둔다 */
|
|
191
|
-
button[dock] {
|
|
192
|
-
flex: none;
|
|
193
|
-
display: flex;
|
|
194
|
-
align-items: center;
|
|
195
|
-
gap: 4px;
|
|
196
|
-
padding: 5px 10px;
|
|
197
|
-
font: var(--label-font, inherit);
|
|
198
|
-
font-size: 0.78rem;
|
|
199
|
-
color: var(--md-sys-color-on-surface-variant);
|
|
200
|
-
background: none;
|
|
201
|
-
border: 1px solid var(--md-sys-color-outline-variant);
|
|
202
|
-
border-radius: 6px;
|
|
203
|
-
cursor: pointer;
|
|
204
|
-
}
|
|
205
|
-
button[dock][on] {
|
|
206
|
-
color: var(--md-sys-color-primary);
|
|
207
|
-
border-color: var(--md-sys-color-primary);
|
|
208
|
-
background-color: var(--md-sys-color-surface-container);
|
|
209
|
-
}
|
|
210
|
-
button[dock] md-icon {
|
|
211
|
-
font-size: 16px;
|
|
212
|
-
}
|
|
213
159
|
|
|
214
160
|
/*
|
|
215
161
|
**세 칸의 자리를 못 박는다.**
|
|
@@ -508,6 +454,39 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
508
454
|
cursor: default;
|
|
509
455
|
}
|
|
510
456
|
|
|
457
|
+
/* AI의 평가는 판정문이 아니라 수락 전에 읽을 근거다. 숨기지 않고 변경 칩 위에 둔다. */
|
|
458
|
+
div[quality-review] {
|
|
459
|
+
padding: 7px var(--spacing-large, 12px);
|
|
460
|
+
background-color: var(--md-sys-color-primary-container);
|
|
461
|
+
border-bottom: 1px solid var(--md-sys-color-outline-variant);
|
|
462
|
+
color: var(--md-sys-color-on-primary-container);
|
|
463
|
+
font-size: 0.72rem;
|
|
464
|
+
line-height: 1.45;
|
|
465
|
+
}
|
|
466
|
+
div[quality-review][review] {
|
|
467
|
+
background-color: var(--md-sys-color-tertiary-container);
|
|
468
|
+
color: var(--md-sys-color-on-tertiary-container);
|
|
469
|
+
}
|
|
470
|
+
div[quality-review] strong { display: block; font-size: 0.74rem; }
|
|
471
|
+
div[quality-review] ul { margin: 4px 0 0; padding-left: 18px; }
|
|
472
|
+
div[proposal-note] {
|
|
473
|
+
padding: 6px var(--spacing-large, 12px) 8px;
|
|
474
|
+
border-bottom: 1px solid var(--md-sys-color-outline-variant);
|
|
475
|
+
background-color: var(--md-sys-color-surface-container-low);
|
|
476
|
+
}
|
|
477
|
+
div[proposal-note] label { display: block; font-size: 0.71rem; color: var(--md-sys-color-on-surface-variant); }
|
|
478
|
+
div[proposal-note] input {
|
|
479
|
+
box-sizing: border-box;
|
|
480
|
+
width: 100%;
|
|
481
|
+
margin-top: 4px;
|
|
482
|
+
padding: 5px 7px;
|
|
483
|
+
border: 1px solid var(--md-sys-color-outline-variant);
|
|
484
|
+
border-radius: 5px;
|
|
485
|
+
color: var(--md-sys-color-on-surface);
|
|
486
|
+
background: var(--md-sys-color-surface);
|
|
487
|
+
font: inherit;
|
|
488
|
+
}
|
|
489
|
+
|
|
511
490
|
/*
|
|
512
491
|
무엇을 바꾸자는 것인가 — 칩 하나가 변경 하나다.
|
|
513
492
|
|
|
@@ -568,6 +547,31 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
568
547
|
this.folded = this.board ? toFigureSource(this.board, this.parts) : undefined;
|
|
569
548
|
}
|
|
570
549
|
}
|
|
550
|
+
/**
|
|
551
|
+
* 페이지 밖 AI 도크에도 **지금 손에 든 정본**을 알려 준다.
|
|
552
|
+
*
|
|
553
|
+
* 모델러 안의 `figure-ask` 는 이미 `folded`를 직접 받는다. 반면 asidebar 도크는 주소의 id만
|
|
554
|
+
* 알았으므로 DB에 저장된 옛 판을 읽었다. 저장 전 변경을 두고 "왜 발행이 안 되나" 또는
|
|
555
|
+
* "이 부품을 줄여 달라"고 물으면 대화가 엉뚱한 판을 보게 되는 결함이었다.
|
|
556
|
+
*
|
|
557
|
+
* 이 이벤트는 저장하지 않는다. 도크가 대화 인자로만 잠시 들고 있고, 서버 도구도 도메인 소속
|
|
558
|
+
* Figure를 먼저 확인한 뒤에만 이 source를 사용한다. 페이지를 떠날 때는 아래에서 지워 오래된
|
|
559
|
+
* 초안이 다음 Figure에 섞이지 않게 한다.
|
|
560
|
+
*/
|
|
561
|
+
updated(changed) {
|
|
562
|
+
if (changed.has('figure') && changed.get('figure')?.id !== this.figure?.id) {
|
|
563
|
+
this.proposalFeedback = [];
|
|
564
|
+
}
|
|
565
|
+
if (changed.has('board') || changed.has('parts') || changed.has('figure')) {
|
|
566
|
+
window.dispatchEvent(new CustomEvent('figure-ai-context', {
|
|
567
|
+
detail: { figureId: this.figure?.id, source: this.folded }
|
|
568
|
+
}));
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
disconnectedCallback() {
|
|
572
|
+
window.dispatchEvent(new CustomEvent('figure-ai-context', { detail: undefined }));
|
|
573
|
+
super.disconnectedCallback();
|
|
574
|
+
}
|
|
571
575
|
get context() {
|
|
572
576
|
return {
|
|
573
577
|
title: this.figure?.name ?? i18next.t('figure.title.new-figure'),
|
|
@@ -601,17 +605,10 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
601
605
|
<figure-ask
|
|
602
606
|
.source=${source}
|
|
603
607
|
.type=${source?.type ?? ''}
|
|
608
|
+
.feedback=${this.proposalFeedback}
|
|
604
609
|
@proposed=${(e) => this.receive(e.detail.proposal)}
|
|
605
610
|
></figure-ask>
|
|
606
611
|
${this.saveFailure ? html `<span save-failed title=${this.saveFailure}>${this.saveFailure}</span>` : ''}
|
|
607
|
-
<button
|
|
608
|
-
dock
|
|
609
|
-
?on=${this.dockOpen}
|
|
610
|
-
title=${i18next.t('figure.text.dock-explained')}
|
|
611
|
-
@click=${() => (this.dockOpen = !this.dockOpen)}
|
|
612
|
-
>
|
|
613
|
-
<md-icon>forum</md-icon>${i18next.t('figure.button.dock')}
|
|
614
|
-
</button>
|
|
615
612
|
<button save ?disabled=${!this.canSave} @click=${() => this.save()}>
|
|
616
613
|
${this.saving ? i18next.t('figure.text.saving-figure') : i18next.t('figure.button.save')}
|
|
617
614
|
</button>
|
|
@@ -621,10 +618,12 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
621
618
|
edits
|
|
622
619
|
?collapse-left=${this.collapsed.left}
|
|
623
620
|
?collapse-right=${this.collapsed.right}
|
|
624
|
-
?dock=${this.dockOpen}
|
|
625
621
|
@figure-rename=${(e) => this.rename(e.detail.name)}
|
|
622
|
+
@figure-catalog=${(e) => this.catalog(e.detail)}
|
|
626
623
|
@figure-released=${(e) => this.released(e.detail.figure)}
|
|
627
624
|
@figure-reverted=${() => this.figure?.id && this.load(this.figure.id)}
|
|
625
|
+
@preview-sizing-fix=${(e) => this.previewSizingFix(e.detail)}
|
|
626
|
+
@apply-sizing-fix=${(e) => this.applySizingFix(e.detail)}
|
|
628
627
|
@add-part=${(e) => this.addPart(e.detail.primitive)}
|
|
629
628
|
@select-part=${(e) => (this.selected = e.detail.index)}
|
|
630
629
|
@remove-part=${(e) => this.removePart(e.detail.index)}
|
|
@@ -648,8 +647,8 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
648
647
|
|
|
649
648
|
<div middle>
|
|
650
649
|
${this.renderBar()}
|
|
651
|
-
${this.
|
|
652
|
-
${this.
|
|
650
|
+
${this.proposalSession ? this.renderDecision() : ''}
|
|
651
|
+
${this.proposalSession
|
|
653
652
|
? html `
|
|
654
653
|
<div lanes>
|
|
655
654
|
<div lane>
|
|
@@ -664,10 +663,11 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
664
663
|
<div lane candidate>
|
|
665
664
|
<h4>
|
|
666
665
|
${i18next.t('figure.label.proposed')}
|
|
667
|
-
${this.
|
|
666
|
+
${this.proposalSession.metrics
|
|
668
667
|
? html `<small>
|
|
669
|
-
${this.
|
|
670
|
-
${i18next.t('figure.text.n-triangles', { count: this.
|
|
668
|
+
${this.proposalSession.metrics.grade} ·
|
|
669
|
+
${i18next.t('figure.text.n-triangles', { count: this.proposalSession.metrics.triangles })}
|
|
670
|
+
${this.proposalSession.metrics.quality ? html ` · ${this.proposalSession.metrics.quality.summary}` : ''}
|
|
671
671
|
</small>`
|
|
672
672
|
: ''}
|
|
673
673
|
</h4>
|
|
@@ -686,6 +686,8 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
686
686
|
|
|
687
687
|
<figure-side
|
|
688
688
|
.figureName=${this.figure?.name ?? ''}
|
|
689
|
+
.figureCategory=${this.figure?.category}
|
|
690
|
+
.figureTags=${this.figure?.tags}
|
|
689
691
|
.figureId=${this.figure?.id ?? ''}
|
|
690
692
|
.figureState=${this.figure?.state}
|
|
691
693
|
.figureVersion=${this.figure?.version ?? 0}
|
|
@@ -697,66 +699,9 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
697
699
|
.view=${this.view}
|
|
698
700
|
@update-view=${(e) => (this.view = e.detail.view)}
|
|
699
701
|
></figure-side>
|
|
700
|
-
|
|
701
|
-
<!-- 대화 도크. 무엇을 켜고 끄는지는 renderDock 위에 적어 두었다 -->
|
|
702
|
-
<ox-board-ai-chat
|
|
703
|
-
.boardTools=${false}
|
|
704
|
-
.toolCategories=${['figure-authoring']}
|
|
705
|
-
.hostContext=${this.hostContext}
|
|
706
|
-
.placeholder=${i18next.t('figure.text.dock-placeholder')}
|
|
707
|
-
@board-ai-proposal-execute=${(e) => this.takeProposal(e.detail.proposal)}
|
|
708
|
-
></ox-board-ai-chat>
|
|
709
702
|
</div>
|
|
710
703
|
`;
|
|
711
704
|
}
|
|
712
|
-
/*
|
|
713
|
-
* ── 대화 도크를 무엇으로 켜나 ───────────────────────────────────────────────
|
|
714
|
-
*
|
|
715
|
-
* board 의 컴포넌트를 그대로 쓰지만 **board 인 척하지 않는다.** `boardTools` 를 끄고 figure 의
|
|
716
|
-
* 도구 범주(`figure-authoring`)만 연다. 끄지 않으면 보드 편집 도구가 함께 들려, 모델이 이
|
|
717
|
-
* 화면에서 할 수 없는 일을 시도한다.
|
|
718
|
-
*
|
|
719
|
-
* board 모양의 인자(`currentBoard` · `knownTypes` · `componentSchemas` · `selectedRefids`)는
|
|
720
|
-
* 주지 않는다 — 전부 선택이고, 채우려면 없는 것을 지어내야 한다.
|
|
721
|
-
*/
|
|
722
|
-
/**
|
|
723
|
-
* 어시스턴트가 「지금 무엇을 열고 있나」를 아는 길.
|
|
724
|
-
*
|
|
725
|
-
* 도구가 대상을 안 받았을 때 이 문맥에서 해소한다 — 사용자에게 식별자를 되묻지 않는다.
|
|
726
|
-
* 팔레트도 함께 보낸다: 모르면 모델이 색을 지어내고, 그 후보는 검증을 지나 **그리는 시점에**
|
|
727
|
-
* 죽는다.
|
|
728
|
-
*/
|
|
729
|
-
get hostContext() {
|
|
730
|
-
return {
|
|
731
|
-
surface: 'figure-modeller',
|
|
732
|
-
figureId: this.figure?.id || undefined,
|
|
733
|
-
figureType: this.board?.figureType,
|
|
734
|
-
figureName: this.figure?.name,
|
|
735
|
-
palette: Object.keys(activePalette())
|
|
736
|
-
};
|
|
737
|
-
}
|
|
738
|
-
/**
|
|
739
|
-
* 도크의 후보를 받는다.
|
|
740
|
-
*
|
|
741
|
-
* **후보가 화면에 닿는 길은 하나뿐이다** — 말로 시키는 줄이든 도크든 `receive()` 로 모인다.
|
|
742
|
-
* 두 길을 두면 「지금 / 후보」 두 칸을 두 곳에서 그리게 되고, 어느 쪽이 이기는지 저작자가
|
|
743
|
-
* 매번 생각해야 한다.
|
|
744
|
-
*/
|
|
745
|
-
takeProposal(proposal) {
|
|
746
|
-
const source = proposal?.source;
|
|
747
|
-
if (!source) {
|
|
748
|
-
// 카드는 왔는데 후보가 없다 — 조용히 넘기지 않는다. 저작자가 눌렀으니 답이 있어야 한다.
|
|
749
|
-
this.saveFailure = i18next.t('figure.text.proposal-had-no-source');
|
|
750
|
-
return;
|
|
751
|
-
}
|
|
752
|
-
this.receive({
|
|
753
|
-
source: JSON.stringify(source),
|
|
754
|
-
grade: proposal.score?.grade ?? '',
|
|
755
|
-
triangles: proposal.cost?.triangles ?? 0,
|
|
756
|
-
groups: proposal.cost?.groups ?? 0,
|
|
757
|
-
attempts: proposal.attempts ?? 1
|
|
758
|
-
});
|
|
759
|
-
}
|
|
760
705
|
/**
|
|
761
706
|
* 가운데 칸 위의 띠 — 무엇을 보고 있나와, 양쪽 칸을 접는 손잡이.
|
|
762
707
|
*
|
|
@@ -786,7 +731,7 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
786
731
|
</button>
|
|
787
732
|
`;
|
|
788
733
|
return html `<div modes>
|
|
789
|
-
${this.renderCollapse('left')}${this.
|
|
734
|
+
${this.renderCollapse('left')}${this.proposalSession ? '' : html `${one('edit')}${one('preview')}`}
|
|
790
735
|
<div spacer></div>
|
|
791
736
|
${this.renderCollapse('right')}
|
|
792
737
|
</div>`;
|
|
@@ -817,16 +762,17 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
817
762
|
rememberPanes(this.collapsed);
|
|
818
763
|
}
|
|
819
764
|
/**
|
|
820
|
-
* AI 후보를
|
|
765
|
+
* AI 후보를 실제 배치 크기에서도 본다.
|
|
821
766
|
*
|
|
822
|
-
*
|
|
823
|
-
*
|
|
767
|
+
* 편집 캔버스 한 장으로는 `repeat`·`fixed`·`anchor`가 맞는지 알 수 없다. 후보는 아직
|
|
768
|
+
* 정본이 아니므로 저장하지 않고, `figure-preview`가 같은 FigureSource를 기준 크기와
|
|
769
|
+
* 일곱 가지 확장 축에서 실제 FigureInstance로 세운다. 따라서 저작자는 "좋아 보이는
|
|
770
|
+
* 한 장"이 아니라 배치될 때도 붙어 있는지를 보고 수락한다.
|
|
824
771
|
*/
|
|
825
772
|
renderProposed() {
|
|
826
|
-
if (!this.
|
|
773
|
+
if (!this.proposalSession)
|
|
827
774
|
return '';
|
|
828
|
-
|
|
829
|
-
return html `<figure-canvas .board=${board} .parts=${parts} .viewOnly=${true}></figure-canvas>`;
|
|
775
|
+
return html `<figure-preview .source=${this.proposalSession.source}></figure-preview>`;
|
|
830
776
|
}
|
|
831
777
|
/**
|
|
832
778
|
* 무엇을 받을지 고른다.
|
|
@@ -843,21 +789,34 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
843
789
|
* 안 들고, 우리가 생각 못 한 조합을 막아 주기 때문이다.
|
|
844
790
|
*/
|
|
845
791
|
renderDecision() {
|
|
846
|
-
if (!this.
|
|
792
|
+
if (!this.proposalSession)
|
|
847
793
|
return '';
|
|
848
|
-
const changes = proposals.diffProposal(this.folded, this.
|
|
849
|
-
const picked = proposals.applyProposal(this.folded, this.
|
|
794
|
+
const changes = proposals.diffProposal(this.folded, this.proposalSession.source);
|
|
795
|
+
const picked = proposals.applyProposal(this.folded, this.proposalSession.source, this.proposalSession.picked);
|
|
850
796
|
const { errors, violations } = validate(picked);
|
|
851
797
|
return html `
|
|
852
798
|
<div decide>
|
|
853
|
-
<span>${i18next.t('figure.text.assistant-proposed-a-figure')}</span>
|
|
799
|
+
<span>${this.proposalSession.title || i18next.t('figure.text.assistant-proposed-a-figure')}</span>
|
|
854
800
|
<div spacer></div>
|
|
855
801
|
<button drop @click=${() => this.discard()}>${i18next.t('figure.button.discard')}</button>
|
|
856
|
-
<button take ?disabled=${errors.length > 0 || this.picked.size === 0} @click=${() => this.take()}>
|
|
857
|
-
${i18next.t('figure.button.take-n-changes', { n: this.picked.size })}
|
|
802
|
+
<button take ?disabled=${errors.length > 0 || this.proposalSession.picked.size === 0} @click=${() => this.take()}>
|
|
803
|
+
${i18next.t('figure.button.take-n-changes', { n: this.proposalSession.picked.size })}
|
|
858
804
|
</button>
|
|
859
805
|
</div>
|
|
860
806
|
|
|
807
|
+
${this.renderQualityReview()}
|
|
808
|
+
<div proposal-note>
|
|
809
|
+
<label>
|
|
810
|
+
${i18next.t('figure.text.proposal-feedback-note')}
|
|
811
|
+
<input
|
|
812
|
+
maxlength="500"
|
|
813
|
+
.value=${this.proposalSession.note}
|
|
814
|
+
placeholder=${i18next.t('figure.text.proposal-feedback-placeholder')}
|
|
815
|
+
@input=${(event) => (this.proposalSession = { ...this.proposalSession, note: event.target.value })}
|
|
816
|
+
/>
|
|
817
|
+
</label>
|
|
818
|
+
</div>
|
|
819
|
+
|
|
861
820
|
${changes.length > 0
|
|
862
821
|
? html `
|
|
863
822
|
<div changes>
|
|
@@ -872,6 +831,24 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
872
831
|
: ''}
|
|
873
832
|
`;
|
|
874
833
|
}
|
|
834
|
+
/** 후보 품질 판단의 근거를 수락 전에 전부 보여 준다. */
|
|
835
|
+
renderQualityReview() {
|
|
836
|
+
const quality = this.proposalSession?.metrics?.quality;
|
|
837
|
+
if (!quality)
|
|
838
|
+
return '';
|
|
839
|
+
return html `
|
|
840
|
+
<div quality-review ?review=${quality.status === 'review'}>
|
|
841
|
+
<strong>${i18next.t('figure.label.candidate-review')}</strong>
|
|
842
|
+
<span>${quality.summary}</span>
|
|
843
|
+
<span> · top-view ${Math.round(quality.visualReadability * 100)}% · ${quality.visualRegions} regions</span>
|
|
844
|
+
${quality.findings.length > 0
|
|
845
|
+
? html `<ul>
|
|
846
|
+
${quality.findings.map(finding => html `<li><code>${finding.dimension}</code> — ${finding.message}</li>`)}
|
|
847
|
+
</ul>`
|
|
848
|
+
: ''}
|
|
849
|
+
</div>
|
|
850
|
+
`;
|
|
851
|
+
}
|
|
875
852
|
/** 변경 하나. 무엇을 하겠다는 것인지 부호로 먼저 말한다. */
|
|
876
853
|
renderChange(change) {
|
|
877
854
|
const icon = {
|
|
@@ -883,7 +860,7 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
883
860
|
anchor: 'my_location',
|
|
884
861
|
styleKit: 'palette'
|
|
885
862
|
}[change.kind];
|
|
886
|
-
const on = this.picked.has(change.key);
|
|
863
|
+
const on = this.proposalSession?.picked.has(change.key);
|
|
887
864
|
return html `
|
|
888
865
|
<button
|
|
889
866
|
change
|
|
@@ -897,12 +874,8 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
897
874
|
`;
|
|
898
875
|
}
|
|
899
876
|
toggle(key) {
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
next.delete(key);
|
|
903
|
-
else
|
|
904
|
-
next.add(key);
|
|
905
|
-
this.picked = next;
|
|
877
|
+
if (this.proposalSession)
|
|
878
|
+
this.proposalSession = proposalSessions.toggleProposalChange(this.proposalSession, key);
|
|
906
879
|
}
|
|
907
880
|
/**
|
|
908
881
|
* 후보가 왔다.
|
|
@@ -911,15 +884,29 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
911
884
|
* 무엇이 자기 것이고 무엇이 기계 것인지 모르게 된다.
|
|
912
885
|
*/
|
|
913
886
|
receive(proposal) {
|
|
914
|
-
this.
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
887
|
+
this.proposalSession = proposalSessions.openAiProposal(this.folded, proposal);
|
|
888
|
+
}
|
|
889
|
+
/**
|
|
890
|
+
* 발행 게이트의 구조화된 수정안 하나를 현재 작업 사본에만 겹쳐 본다. 서버가 준 값은
|
|
891
|
+
* `part.anchor.axis = to`라는 데이터이지 사람이 읽을 문장을 다시 해석한 결과가 아니다.
|
|
892
|
+
* 원본 `board`/`parts`는 여기서 전혀 바꾸지 않고, 아래의 `take()`만 바꾼 사본을 적용한다.
|
|
893
|
+
*/
|
|
894
|
+
previewSizingFix(fix) {
|
|
895
|
+
if (!this.folded)
|
|
896
|
+
return;
|
|
897
|
+
this.proposalSession = proposalSessions.openSizingFix(this.folded, fix);
|
|
898
|
+
}
|
|
899
|
+
/**
|
|
900
|
+
* 원클릭 적용은 서버가 모든 크기 변경 case를 재검사해 안전하다고 표시한 한-앵커 후보에만 연다.
|
|
901
|
+
* 그래도 저장은 저작자의 별도 행위다. 잘못 눌러도 DB 정본이나 발행판은 바뀌지 않는다.
|
|
902
|
+
*/
|
|
903
|
+
applySizingFix(fix) {
|
|
904
|
+
if (!fix.safe || !this.folded)
|
|
905
|
+
return;
|
|
906
|
+
this.previewSizingFix(fix);
|
|
907
|
+
if (!this.proposalSession)
|
|
908
|
+
return;
|
|
909
|
+
this.take();
|
|
923
910
|
}
|
|
924
911
|
/**
|
|
925
912
|
* 고른 변경을 받아들인다.
|
|
@@ -928,20 +915,42 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
928
915
|
* 하므로 그쪽이 자연스럽다. 받아들인 결과는 다시 씬 모델로 되돌려 편집을 이어간다.
|
|
929
916
|
*/
|
|
930
917
|
take() {
|
|
931
|
-
if (!this.
|
|
918
|
+
if (!this.proposalSession)
|
|
932
919
|
return;
|
|
933
|
-
const taken = proposals.applyProposal(this.folded, this.
|
|
920
|
+
const taken = proposals.applyProposal(this.folded, this.proposalSession.source, this.proposalSession.picked);
|
|
934
921
|
const { board, parts } = fromFigureSource(taken);
|
|
935
922
|
this.board = board;
|
|
936
923
|
this.parts = parts;
|
|
937
924
|
this.selected = parts.length ? 0 : -1;
|
|
938
925
|
this.dirty = true;
|
|
939
|
-
this.
|
|
926
|
+
this.reportProposalFeedback('accepted');
|
|
927
|
+
this.discard(false);
|
|
940
928
|
}
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
929
|
+
/**
|
|
930
|
+
* 후보에 대한 사람의 결정을 도크에만 알린다.
|
|
931
|
+
*
|
|
932
|
+
* 이 기록은 분석 이벤트나 Figure 속성이 아니다. 같은 모델러 세션에서 다음 요청을 더 잘
|
|
933
|
+
* 이해시키는 짧은 신호일 뿐이다. 원본 source나 사람의 작업 내용을 다시 보내지 않고, 사람이
|
|
934
|
+
* 후보 변경 중 몇 개를 받아들였는지와 이미 화면에 보였던 품질 판정만 보낸다.
|
|
935
|
+
*/
|
|
936
|
+
reportProposalFeedback(outcome) {
|
|
937
|
+
if (!this.proposalSession)
|
|
938
|
+
return;
|
|
939
|
+
const feedback = proposalSessions.feedbackFromSession(this.folded, this.proposalSession, outcome);
|
|
940
|
+
if (!feedback)
|
|
941
|
+
return;
|
|
942
|
+
this.proposalFeedback = [...this.proposalFeedback, feedback].slice(-5);
|
|
943
|
+
window.dispatchEvent(new CustomEvent('figure-ai-feedback', {
|
|
944
|
+
detail: {
|
|
945
|
+
figureId: this.figure?.id,
|
|
946
|
+
...feedback
|
|
947
|
+
}
|
|
948
|
+
}));
|
|
949
|
+
}
|
|
950
|
+
discard(reportFeedback = true) {
|
|
951
|
+
if (reportFeedback)
|
|
952
|
+
this.reportProposalFeedback('discarded');
|
|
953
|
+
this.proposalSession = undefined;
|
|
945
954
|
}
|
|
946
955
|
/**
|
|
947
956
|
* 저장할 수 있나.
|
|
@@ -1054,6 +1063,18 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
1054
1063
|
this.figure = { ...this.figure, name };
|
|
1055
1064
|
this.dirty = true;
|
|
1056
1065
|
}
|
|
1066
|
+
/**
|
|
1067
|
+
* 분류와 태그를 고친다.
|
|
1068
|
+
*
|
|
1069
|
+
* 정본(`source`)이 아니라 **카탈로그의 값**이다 — 도형이 무엇으로 생겼는지가 아니라 어디에
|
|
1070
|
+
* 걸리는지다. 그래서 점수도 발행도 흔들리지 않는다.
|
|
1071
|
+
*/
|
|
1072
|
+
catalog(patch) {
|
|
1073
|
+
if (!this.figure)
|
|
1074
|
+
return;
|
|
1075
|
+
this.figure = { ...this.figure, ...patch };
|
|
1076
|
+
this.dirty = true;
|
|
1077
|
+
}
|
|
1057
1078
|
/**
|
|
1058
1079
|
* 부품 목록을 갈아 끼운다.
|
|
1059
1080
|
*
|
|
@@ -1245,6 +1266,12 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
1245
1266
|
if (this.figure.id) {
|
|
1246
1267
|
const result = await updateFigure(this.figure.id, {
|
|
1247
1268
|
name: this.figure.name,
|
|
1269
|
+
/*
|
|
1270
|
+
카탈로그의 값도 함께 보낸다. 화면에서 고쳐 놓고 저장에 안 실으면 저장 뒤에 조용히
|
|
1271
|
+
사라진다 — 저작자는 저장했다고 보고 다시 열면 없다.
|
|
1272
|
+
*/
|
|
1273
|
+
category: this.figure.category,
|
|
1274
|
+
tags: this.figure.tags,
|
|
1248
1275
|
source: sourceJson,
|
|
1249
1276
|
thumbnail
|
|
1250
1277
|
});
|
|
@@ -1261,6 +1288,8 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
|
|
|
1261
1288
|
const result = await createFigure({
|
|
1262
1289
|
type: this.folded.type,
|
|
1263
1290
|
name: this.figure.name,
|
|
1291
|
+
category: this.figure.category,
|
|
1292
|
+
tags: this.figure.tags,
|
|
1264
1293
|
source: sourceJson,
|
|
1265
1294
|
thumbnail
|
|
1266
1295
|
});
|
|
@@ -1331,19 +1360,11 @@ __decorate([
|
|
|
1331
1360
|
__decorate([
|
|
1332
1361
|
state(),
|
|
1333
1362
|
__metadata("design:type", Object)
|
|
1334
|
-
], FigureModellerPage.prototype, "
|
|
1335
|
-
__decorate([
|
|
1336
|
-
state(),
|
|
1337
|
-
__metadata("design:type", Object)
|
|
1338
|
-
], FigureModellerPage.prototype, "proposal", void 0);
|
|
1339
|
-
__decorate([
|
|
1340
|
-
state(),
|
|
1341
|
-
__metadata("design:type", Object)
|
|
1342
|
-
], FigureModellerPage.prototype, "proposalOf", void 0);
|
|
1363
|
+
], FigureModellerPage.prototype, "proposalSession", void 0);
|
|
1343
1364
|
__decorate([
|
|
1344
1365
|
state(),
|
|
1345
|
-
__metadata("design:type",
|
|
1346
|
-
], FigureModellerPage.prototype, "
|
|
1366
|
+
__metadata("design:type", Array)
|
|
1367
|
+
], FigureModellerPage.prototype, "proposalFeedback", void 0);
|
|
1347
1368
|
FigureModellerPage = __decorate([
|
|
1348
1369
|
customElement('figure-modeller-page')
|
|
1349
1370
|
], FigureModellerPage);
|