@things-factory/figure-ui 10.1.7 → 10.1.9

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.
Files changed (39) hide show
  1. package/client/graphql/index.ts +24 -0
  2. package/client/modeller/figure-animations.ts +61 -4
  3. package/client/modeller/figure-ask.ts +6 -1
  4. package/client/modeller/figure-history-panel.ts +52 -1
  5. package/client/modeller/figure-inspector.ts +477 -124
  6. package/client/modeller/proposal-session.ts +92 -0
  7. package/client/pages/figure-modeller-page.ts +179 -53
  8. package/client/types.ts +33 -0
  9. package/dist-client/graphql/index.d.ts +3 -1
  10. package/dist-client/graphql/index.js +21 -0
  11. package/dist-client/graphql/index.js.map +1 -1
  12. package/dist-client/modeller/figure-animations.d.ts +4 -1
  13. package/dist-client/modeller/figure-animations.js +57 -4
  14. package/dist-client/modeller/figure-animations.js.map +1 -1
  15. package/dist-client/modeller/figure-ask.d.ts +3 -0
  16. package/dist-client/modeller/figure-ask.js +9 -0
  17. package/dist-client/modeller/figure-ask.js.map +1 -1
  18. package/dist-client/modeller/figure-history-panel.d.ts +7 -0
  19. package/dist-client/modeller/figure-history-panel.js +50 -0
  20. package/dist-client/modeller/figure-history-panel.js.map +1 -1
  21. package/dist-client/modeller/figure-inspector.d.ts +3 -0
  22. package/dist-client/modeller/figure-inspector.js +464 -111
  23. package/dist-client/modeller/figure-inspector.js.map +1 -1
  24. package/dist-client/modeller/proposal-session.d.ts +23 -0
  25. package/dist-client/modeller/proposal-session.js +65 -0
  26. package/dist-client/modeller/proposal-session.js.map +1 -0
  27. package/dist-client/pages/figure-modeller-page.d.ts +43 -9
  28. package/dist-client/pages/figure-modeller-page.js +173 -50
  29. package/dist-client/pages/figure-modeller-page.js.map +1 -1
  30. package/dist-client/tsconfig.tsbuildinfo +1 -1
  31. package/dist-client/types.d.ts +38 -0
  32. package/dist-client/types.js.map +1 -1
  33. package/package.json +4 -4
  34. package/test/ai-proposal-contract.test.ts +169 -0
  35. package/translations/en.json +32 -0
  36. package/translations/ja.json +32 -0
  37. package/translations/ko.json +42 -0
  38. package/translations/ms.json +32 -0
  39. package/translations/zh.json +32 -0
@@ -64,12 +64,10 @@ export declare class FigureModellerPage extends FigureModellerPageBase {
64
64
  * 미저장 표시(`dirty`)는 그대로 남으므로 데이터를 잃지는 않았다. 잃은 것은 **사유**다.
65
65
  */
66
66
  private saveFailure;
67
- /** 저작 보조가 후보. 받기 전에는 원본 데이터가 아니다. */
68
- private proposal?;
69
- /** 후보의 셈. 무엇을 받는 것인지 숫자로도 보여 준다. */
70
- private proposalOf?;
71
- /** 받기로 고른 변경들. 처음에는 전부 골라 둔다. */
72
- private picked;
67
+ /** 저작 보조 또는 발행 게이트가 연 후보 검토 세션. 받기 전에는 정본이 아니다. */
68
+ private proposalSession?;
69
+ /** 인라인 요청창과 AI 도크가 함께 쓰는 현재 Figure의 제한된 세션 피드백. */
70
+ private proposalFeedback;
73
71
  /**
74
72
  * 저장 형식으로 변환한 것. 씬 모델이 바뀔 때만 다시 만든다.
75
73
  *
@@ -78,6 +76,19 @@ export declare class FigureModellerPage extends FigureModellerPageBase {
78
76
  */
79
77
  private folded?;
80
78
  willUpdate(changed: Map<string, unknown>): void;
79
+ /**
80
+ * 페이지 밖 AI 도크에도 **지금 손에 든 정본**을 알려 준다.
81
+ *
82
+ * 모델러 안의 `figure-ask` 는 이미 `folded`를 직접 받는다. 반면 asidebar 도크는 주소의 id만
83
+ * 알았으므로 DB에 저장된 옛 판을 읽었다. 저장 전 변경을 두고 "왜 발행이 안 되나" 또는
84
+ * "이 부품을 줄여 달라"고 물으면 대화가 엉뚱한 판을 보게 되는 결함이었다.
85
+ *
86
+ * 이 이벤트는 저장하지 않는다. 도크가 대화 인자로만 잠시 들고 있고, 서버 도구도 도메인 소속
87
+ * Figure를 먼저 확인한 뒤에만 이 source를 사용한다. 페이지를 떠날 때는 아래에서 지워 오래된
88
+ * 초안이 다음 Figure에 섞이지 않게 한다.
89
+ */
90
+ updated(changed: Map<string, unknown>): void;
91
+ disconnectedCallback(): void;
81
92
  get context(): {
82
93
  title: string;
83
94
  heading: boolean;
@@ -110,10 +121,12 @@ export declare class FigureModellerPage extends FigureModellerPageBase {
110
121
  private renderCollapse;
111
122
  private toggleCollapse;
112
123
  /**
113
- * AI 후보를 나란히 세운다.
124
+ * AI 후보를 실제 배치 크기에서도 본다.
114
125
  *
115
- * 후보는 저장 형식으로 온다. 미리보기는 모델을 받으므로 여기서 번 변환한다 —
116
- * 후보를 받아들이기 전까지는 편집 데이터가 아니므로 상태로 두지 않는다.
126
+ * 편집 캔버스 장으로는 `repeat`·`fixed`·`anchor`가 맞는지 없다. 후보는 아직
127
+ * 정본이 아니므로 저장하지 않고, `figure-preview`가 같은 FigureSource를 기준 크기와
128
+ * 일곱 가지 확장 축에서 실제 FigureInstance로 세운다. 따라서 저작자는 "좋아 보이는
129
+ * 한 장"이 아니라 배치될 때도 붙어 있는지를 보고 수락한다.
117
130
  */
118
131
  private renderProposed;
119
132
  /**
@@ -131,6 +144,8 @@ export declare class FigureModellerPage extends FigureModellerPageBase {
131
144
  * 안 들고, 우리가 생각 못 한 조합을 막아 주기 때문이다.
132
145
  */
133
146
  private renderDecision;
147
+ /** 후보 품질 판단의 근거를 수락 전에 전부 보여 준다. */
148
+ private renderQualityReview;
134
149
  /** 변경 하나. 무엇을 하겠다는 것인지 부호로 먼저 말한다. */
135
150
  private renderChange;
136
151
  private toggle;
@@ -141,6 +156,17 @@ export declare class FigureModellerPage extends FigureModellerPageBase {
141
156
  * 무엇이 자기 것이고 무엇이 기계 것인지 모르게 된다.
142
157
  */
143
158
  private receive;
159
+ /**
160
+ * 발행 게이트의 구조화된 수정안 하나를 현재 작업 사본에만 겹쳐 본다. 서버가 준 값은
161
+ * `part.anchor.axis = to`라는 데이터이지 사람이 읽을 문장을 다시 해석한 결과가 아니다.
162
+ * 원본 `board`/`parts`는 여기서 전혀 바꾸지 않고, 아래의 `take()`만 바꾼 사본을 적용한다.
163
+ */
164
+ private previewSizingFix;
165
+ /**
166
+ * 원클릭 적용은 서버가 모든 크기 변경 case를 재검사해 안전하다고 표시한 한-앵커 후보에만 연다.
167
+ * 그래도 저장은 저작자의 별도 행위다. 잘못 눌러도 DB 정본이나 발행판은 바뀌지 않는다.
168
+ */
169
+ private applySizingFix;
144
170
  /**
145
171
  * 고른 변경을 받아들인다.
146
172
  *
@@ -148,6 +174,14 @@ export declare class FigureModellerPage extends FigureModellerPageBase {
148
174
  * 하므로 그쪽이 자연스럽다. 받아들인 결과는 다시 씬 모델로 되돌려 편집을 이어간다.
149
175
  */
150
176
  private take;
177
+ /**
178
+ * 후보에 대한 사람의 결정을 도크에만 알린다.
179
+ *
180
+ * 이 기록은 분석 이벤트나 Figure 속성이 아니다. 같은 모델러 세션에서 다음 요청을 더 잘
181
+ * 이해시키는 짧은 신호일 뿐이다. 원본 source나 사람의 작업 내용을 다시 보내지 않고, 사람이
182
+ * 후보 변경 중 몇 개를 받아들였는지와 이미 화면에 보였던 품질 판정만 보낸다.
183
+ */
184
+ private reportProposalFeedback;
151
185
  private discard;
152
186
  /**
153
187
  * 저장할 수 있나.
@@ -14,6 +14,7 @@ import { navigate, PageView } from '@operato/shell';
14
14
  import { validate } from '@hatiolab/figure-model';
15
15
  import * as edits from '../modeller/part-edits.js';
16
16
  import * as proposals from '../modeller/proposal.js';
17
+ import * as proposalSessions from '../modeller/proposal-session.js';
17
18
  import { toFigureSource, fromFigureSource } from '../modeller/figure-source.js';
18
19
  import { DEFAULT_VIEW } from '../modeller/figure-view.js';
19
20
  import { createFigure, fetchFigure, fetchFigureTypeNames, updateFigure } from '../graphql/index.js';
@@ -126,8 +127,8 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
126
127
  * 미저장 표시(`dirty`)는 그대로 남으므로 데이터를 잃지는 않았다. 잃은 것은 **사유**다.
127
128
  */
128
129
  this.saveFailure = '';
129
- /** 받기로 고른 변경들. 처음에는 전부 골라 둔다. */
130
- this.picked = new Set();
130
+ /** 인라인 요청창과 AI 도크가 함께 쓰는 현재 Figure의 제한된 세션 피드백. */
131
+ this.proposalFeedback = [];
131
132
  }
132
133
  static { this.styles = css `
133
134
  :host {
@@ -453,6 +454,39 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
453
454
  cursor: default;
454
455
  }
455
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
+
456
490
  /*
457
491
  무엇을 바꾸자는 것인가 — 칩 하나가 변경 하나다.
458
492
 
@@ -513,6 +547,31 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
513
547
  this.folded = this.board ? toFigureSource(this.board, this.parts) : undefined;
514
548
  }
515
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
+ }
516
575
  get context() {
517
576
  return {
518
577
  title: this.figure?.name ?? i18next.t('figure.title.new-figure'),
@@ -546,6 +605,7 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
546
605
  <figure-ask
547
606
  .source=${source}
548
607
  .type=${source?.type ?? ''}
608
+ .feedback=${this.proposalFeedback}
549
609
  @proposed=${(e) => this.receive(e.detail.proposal)}
550
610
  ></figure-ask>
551
611
  ${this.saveFailure ? html `<span save-failed title=${this.saveFailure}>${this.saveFailure}</span>` : ''}
@@ -562,6 +622,8 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
562
622
  @figure-catalog=${(e) => this.catalog(e.detail)}
563
623
  @figure-released=${(e) => this.released(e.detail.figure)}
564
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)}
565
627
  @add-part=${(e) => this.addPart(e.detail.primitive)}
566
628
  @select-part=${(e) => (this.selected = e.detail.index)}
567
629
  @remove-part=${(e) => this.removePart(e.detail.index)}
@@ -585,8 +647,8 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
585
647
 
586
648
  <div middle>
587
649
  ${this.renderBar()}
588
- ${this.proposal ? this.renderDecision() : ''}
589
- ${this.proposal
650
+ ${this.proposalSession ? this.renderDecision() : ''}
651
+ ${this.proposalSession
590
652
  ? html `
591
653
  <div lanes>
592
654
  <div lane>
@@ -601,10 +663,11 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
601
663
  <div lane candidate>
602
664
  <h4>
603
665
  ${i18next.t('figure.label.proposed')}
604
- ${this.proposalOf
666
+ ${this.proposalSession.metrics
605
667
  ? html `<small>
606
- ${this.proposalOf.grade} ·
607
- ${i18next.t('figure.text.n-triangles', { count: this.proposalOf.triangles })}
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}` : ''}
608
671
  </small>`
609
672
  : ''}
610
673
  </h4>
@@ -668,7 +731,7 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
668
731
  </button>
669
732
  `;
670
733
  return html `<div modes>
671
- ${this.renderCollapse('left')}${this.proposal ? '' : html `${one('edit')}${one('preview')}`}
734
+ ${this.renderCollapse('left')}${this.proposalSession ? '' : html `${one('edit')}${one('preview')}`}
672
735
  <div spacer></div>
673
736
  ${this.renderCollapse('right')}
674
737
  </div>`;
@@ -699,16 +762,17 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
699
762
  rememberPanes(this.collapsed);
700
763
  }
701
764
  /**
702
- * AI 후보를 나란히 세운다.
765
+ * AI 후보를 실제 배치 크기에서도 본다.
703
766
  *
704
- * 후보는 저장 형식으로 온다. 미리보기는 모델을 받으므로 여기서 번 변환한다 —
705
- * 후보를 받아들이기 전까지는 편집 데이터가 아니므로 상태로 두지 않는다.
767
+ * 편집 캔버스 장으로는 `repeat`·`fixed`·`anchor`가 맞는지 없다. 후보는 아직
768
+ * 정본이 아니므로 저장하지 않고, `figure-preview`가 같은 FigureSource를 기준 크기와
769
+ * 일곱 가지 확장 축에서 실제 FigureInstance로 세운다. 따라서 저작자는 "좋아 보이는
770
+ * 한 장"이 아니라 배치될 때도 붙어 있는지를 보고 수락한다.
706
771
  */
707
772
  renderProposed() {
708
- if (!this.proposal)
773
+ if (!this.proposalSession)
709
774
  return '';
710
- const { board, parts } = fromFigureSource(this.proposal);
711
- return html `<figure-canvas .board=${board} .parts=${parts} .viewOnly=${true}></figure-canvas>`;
775
+ return html `<figure-preview .source=${this.proposalSession.source}></figure-preview>`;
712
776
  }
713
777
  /**
714
778
  * 무엇을 받을지 고른다.
@@ -725,21 +789,34 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
725
789
  * 안 들고, 우리가 생각 못 한 조합을 막아 주기 때문이다.
726
790
  */
727
791
  renderDecision() {
728
- if (!this.proposal)
792
+ if (!this.proposalSession)
729
793
  return '';
730
- const changes = proposals.diffProposal(this.folded, this.proposal);
731
- const picked = proposals.applyProposal(this.folded, this.proposal, this.picked);
794
+ const changes = proposals.diffProposal(this.folded, this.proposalSession.source);
795
+ const picked = proposals.applyProposal(this.folded, this.proposalSession.source, this.proposalSession.picked);
732
796
  const { errors, violations } = validate(picked);
733
797
  return html `
734
798
  <div decide>
735
- <span>${i18next.t('figure.text.assistant-proposed-a-figure')}</span>
799
+ <span>${this.proposalSession.title || i18next.t('figure.text.assistant-proposed-a-figure')}</span>
736
800
  <div spacer></div>
737
801
  <button drop @click=${() => this.discard()}>${i18next.t('figure.button.discard')}</button>
738
- <button take ?disabled=${errors.length > 0 || this.picked.size === 0} @click=${() => this.take()}>
739
- ${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 })}
740
804
  </button>
741
805
  </div>
742
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
+
743
820
  ${changes.length > 0
744
821
  ? html `
745
822
  <div changes>
@@ -754,6 +831,24 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
754
831
  : ''}
755
832
  `;
756
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
+ }
757
852
  /** 변경 하나. 무엇을 하겠다는 것인지 부호로 먼저 말한다. */
758
853
  renderChange(change) {
759
854
  const icon = {
@@ -765,7 +860,7 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
765
860
  anchor: 'my_location',
766
861
  styleKit: 'palette'
767
862
  }[change.kind];
768
- const on = this.picked.has(change.key);
863
+ const on = this.proposalSession?.picked.has(change.key);
769
864
  return html `
770
865
  <button
771
866
  change
@@ -779,12 +874,8 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
779
874
  `;
780
875
  }
781
876
  toggle(key) {
782
- const next = new Set(this.picked);
783
- if (next.has(key))
784
- next.delete(key);
785
- else
786
- next.add(key);
787
- this.picked = next;
877
+ if (this.proposalSession)
878
+ this.proposalSession = proposalSessions.toggleProposalChange(this.proposalSession, key);
788
879
  }
789
880
  /**
790
881
  * 후보가 왔다.
@@ -793,15 +884,29 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
793
884
  * 무엇이 자기 것이고 무엇이 기계 것인지 모르게 된다.
794
885
  */
795
886
  receive(proposal) {
796
- this.proposal = JSON.parse(proposal.source);
797
- // 처음에는 전부 골라 둔다 — 통째로 받는 것이 보통의 길이다.
798
- this.picked = new Set(proposals.diffProposal(this.folded, this.proposal).map(change => change.key));
799
- this.proposalOf = {
800
- grade: proposal.grade,
801
- triangles: proposal.triangles,
802
- groups: proposal.groups,
803
- attempts: proposal.attempts
804
- };
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();
805
910
  }
806
911
  /**
807
912
  * 고른 변경을 받아들인다.
@@ -810,20 +915,42 @@ let FigureModellerPage = class FigureModellerPage extends FigureModellerPageBase
810
915
  * 하므로 그쪽이 자연스럽다. 받아들인 결과는 다시 씬 모델로 되돌려 편집을 이어간다.
811
916
  */
812
917
  take() {
813
- if (!this.proposal)
918
+ if (!this.proposalSession)
814
919
  return;
815
- const taken = proposals.applyProposal(this.folded, this.proposal, this.picked);
920
+ const taken = proposals.applyProposal(this.folded, this.proposalSession.source, this.proposalSession.picked);
816
921
  const { board, parts } = fromFigureSource(taken);
817
922
  this.board = board;
818
923
  this.parts = parts;
819
924
  this.selected = parts.length ? 0 : -1;
820
925
  this.dirty = true;
821
- this.discard();
926
+ this.reportProposalFeedback('accepted');
927
+ this.discard(false);
822
928
  }
823
- discard() {
824
- this.proposal = undefined;
825
- this.proposalOf = undefined;
826
- this.picked = new Set();
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;
827
954
  }
828
955
  /**
829
956
  * 저장할 수 있나.
@@ -1233,15 +1360,11 @@ __decorate([
1233
1360
  __decorate([
1234
1361
  state(),
1235
1362
  __metadata("design:type", Object)
1236
- ], FigureModellerPage.prototype, "proposal", void 0);
1237
- __decorate([
1238
- state(),
1239
- __metadata("design:type", Object)
1240
- ], FigureModellerPage.prototype, "proposalOf", void 0);
1363
+ ], FigureModellerPage.prototype, "proposalSession", void 0);
1241
1364
  __decorate([
1242
1365
  state(),
1243
- __metadata("design:type", Object)
1244
- ], FigureModellerPage.prototype, "picked", void 0);
1366
+ __metadata("design:type", Array)
1367
+ ], FigureModellerPage.prototype, "proposalFeedback", void 0);
1245
1368
  FigureModellerPage = __decorate([
1246
1369
  customElement('figure-modeller-page')
1247
1370
  ], FigureModellerPage);