@things-factory/figure-ui 10.1.7 → 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 +24 -0
- package/client/modeller/figure-animations.ts +61 -4
- package/client/modeller/figure-ask.ts +6 -1
- package/client/modeller/figure-history-panel.ts +52 -1
- package/client/modeller/figure-inspector.ts +477 -124
- package/client/modeller/proposal-session.ts +92 -0
- package/client/pages/figure-modeller-page.ts +179 -53
- package/client/types.ts +33 -0
- package/dist-client/graphql/index.d.ts +3 -1
- package/dist-client/graphql/index.js +21 -0
- package/dist-client/graphql/index.js.map +1 -1
- package/dist-client/modeller/figure-animations.d.ts +4 -1
- package/dist-client/modeller/figure-animations.js +57 -4
- package/dist-client/modeller/figure-animations.js.map +1 -1
- 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 +3 -0
- package/dist-client/modeller/figure-inspector.js +464 -111
- package/dist-client/modeller/figure-inspector.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-modeller-page.d.ts +43 -9
- package/dist-client/pages/figure-modeller-page.js +173 -50
- package/dist-client/pages/figure-modeller-page.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/types.d.ts +38 -0
- package/dist-client/types.js.map +1 -1
- package/package.json +4 -4
- package/test/ai-proposal-contract.test.ts +169 -0
- package/translations/en.json +32 -0
- package/translations/ja.json +32 -0
- package/translations/ko.json +42 -0
- package/translations/ms.json +32 -0
- package/translations/zh.json +32 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { FigureSource } from '@hatiolab/figure-model'
|
|
2
|
+
|
|
3
|
+
import type { FigureGateFix, FigureProposal, ProposalFeedback } from '../types.js'
|
|
4
|
+
import { diffProposal } from './proposal.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 한 번의 AI 후보 검토에만 존재하는 상태다.
|
|
8
|
+
*
|
|
9
|
+
* 후보는 정본이 아니다. 후보 원본·선택·메모·측정값을 한 값으로 묶어 두면 화면이
|
|
10
|
+
* 수락/거절 중 일부만 이전 후보에서 남기는 일이 없다.
|
|
11
|
+
*/
|
|
12
|
+
export interface ProposalSession {
|
|
13
|
+
source: FigureSource
|
|
14
|
+
title: string
|
|
15
|
+
picked: Set<string>
|
|
16
|
+
note: string
|
|
17
|
+
metrics?: Pick<FigureProposal, 'grade' | 'triangles' | 'groups' | 'attempts' | 'quality'>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function selectedKeys(current: FigureSource | undefined, source: FigureSource): Set<string> {
|
|
21
|
+
return new Set(diffProposal(current, source).map(change => change.key))
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** 서버 후보를 검토 세션으로 연다. 저장 형식이 아니면 화면에 후보를 열지 않는다. */
|
|
25
|
+
export function openAiProposal(current: FigureSource | undefined, proposal: FigureProposal): ProposalSession {
|
|
26
|
+
const source = JSON.parse(proposal.source) as FigureSource
|
|
27
|
+
if (!source || !Array.isArray(source.parts)) {
|
|
28
|
+
throw new Error('AI 후보의 Figure 형식이 올바르지 않습니다.')
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
source,
|
|
33
|
+
title: '',
|
|
34
|
+
picked: selectedKeys(current, source),
|
|
35
|
+
note: '',
|
|
36
|
+
metrics: {
|
|
37
|
+
grade: proposal.grade,
|
|
38
|
+
triangles: proposal.triangles,
|
|
39
|
+
groups: proposal.groups,
|
|
40
|
+
attempts: proposal.attempts,
|
|
41
|
+
quality: proposal.quality
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** 발행 게이트가 측정한 한-앵커 수정도 같은 후보 세션으로 연다. */
|
|
47
|
+
export function openSizingFix(current: FigureSource, fix: FigureGateFix): ProposalSession | undefined {
|
|
48
|
+
if (!current.parts.some(part => part.name === fix.part)) return undefined
|
|
49
|
+
|
|
50
|
+
const source: FigureSource = {
|
|
51
|
+
...current,
|
|
52
|
+
parts: current.parts.map(part =>
|
|
53
|
+
part.name === fix.part ? { ...part, anchor: { ...(part.anchor ?? {}), [fix.axis]: fix.to } } : part
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
source,
|
|
59
|
+
title: `발행 검사 수정안 · ${fix.part}.anchor.${fix.axis}: ${fix.from ?? 'auto'} → ${fix.to}`,
|
|
60
|
+
picked: selectedKeys(current, source),
|
|
61
|
+
note: ''
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** 후보 검토 중 사람의 선택을 뒤집는다. 기존 Set을 바꾸지 않아 Lit 상태 경계도 보존한다. */
|
|
66
|
+
export function toggleProposalChange(session: ProposalSession, key: string): ProposalSession {
|
|
67
|
+
const picked = new Set(session.picked)
|
|
68
|
+
if (picked.has(key)) picked.delete(key)
|
|
69
|
+
else picked.add(key)
|
|
70
|
+
return { ...session, picked }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** 사람의 결정을, 다음 AI 요청에만 쓰는 제한된 피드백으로 만든다. */
|
|
74
|
+
export function feedbackFromSession(
|
|
75
|
+
current: FigureSource | undefined,
|
|
76
|
+
session: ProposalSession,
|
|
77
|
+
outcome: ProposalFeedback['outcome']
|
|
78
|
+
): ProposalFeedback | undefined {
|
|
79
|
+
if (!session.metrics) return undefined
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
outcome,
|
|
83
|
+
selectedChanges: outcome === 'accepted' ? session.picked.size : 0,
|
|
84
|
+
totalChanges: diffProposal(current, session.source).length,
|
|
85
|
+
note: session.note.trim().slice(0, 500) || undefined,
|
|
86
|
+
grade: session.metrics.grade,
|
|
87
|
+
quality: {
|
|
88
|
+
status: session.metrics.quality.status,
|
|
89
|
+
findingCodes: session.metrics.quality.findings.map(finding => finding.code)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -17,12 +17,14 @@ import type { DetailLevel, FigureSource, PrimitiveKind } from '@hatiolab/figure-
|
|
|
17
17
|
|
|
18
18
|
import * as edits from '../modeller/part-edits.js'
|
|
19
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'
|
|
20
22
|
import { toFigureSource, fromFigureSource } from '../modeller/figure-source.js'
|
|
21
23
|
import type { BoardModel, PartModel, PlaneAxis } from '../modeller/figure-source.js'
|
|
22
24
|
import { DEFAULT_VIEW } from '../modeller/figure-view.js'
|
|
23
25
|
import type { ViewSettings } from '../modeller/figure-view.js'
|
|
24
26
|
import { createFigure, fetchFigure, fetchFigureTypeNames, updateFigure } from '../graphql/index.js'
|
|
25
|
-
import type { Figure, FigureFinding } from '../types.js'
|
|
27
|
+
import type { Figure, FigureFinding, FigureGateFix, FigureProposal, ProposalFeedback } from '../types.js'
|
|
26
28
|
|
|
27
29
|
const FigureModellerPageBase = localize(i18next)(PageView) as typeof PageView
|
|
28
30
|
|
|
@@ -436,6 +438,39 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
436
438
|
cursor: default;
|
|
437
439
|
}
|
|
438
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
|
+
|
|
439
474
|
/*
|
|
440
475
|
무엇을 바꾸자는 것인가 — 칩 하나가 변경 하나다.
|
|
441
476
|
|
|
@@ -531,12 +566,10 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
531
566
|
* 미저장 표시(`dirty`)는 그대로 남으므로 데이터를 잃지는 않았다. 잃은 것은 **사유**다.
|
|
532
567
|
*/
|
|
533
568
|
@state() private saveFailure = ''
|
|
534
|
-
/** 저작
|
|
535
|
-
@state() private
|
|
536
|
-
/**
|
|
537
|
-
@state() private
|
|
538
|
-
/** 받기로 고른 변경들. 처음에는 전부 골라 둔다. */
|
|
539
|
-
@state() private picked = new Set<string>()
|
|
569
|
+
/** 저작 보조 또는 발행 게이트가 연 후보 검토 세션. 받기 전에는 정본이 아니다. */
|
|
570
|
+
@state() private proposalSession?: ProposalSession
|
|
571
|
+
/** 인라인 요청창과 AI 도크가 함께 쓰는 현재 Figure의 제한된 세션 피드백. */
|
|
572
|
+
@state() private proposalFeedback: ProposalFeedback[] = []
|
|
540
573
|
|
|
541
574
|
/**
|
|
542
575
|
* 저장 형식으로 변환한 것. 씬 모델이 바뀔 때만 다시 만든다.
|
|
@@ -552,6 +585,35 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
552
585
|
}
|
|
553
586
|
}
|
|
554
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
|
+
|
|
555
617
|
get context() {
|
|
556
618
|
return {
|
|
557
619
|
title: this.figure?.name ?? i18next.t('figure.title.new-figure'),
|
|
@@ -587,6 +649,7 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
587
649
|
<figure-ask
|
|
588
650
|
.source=${source}
|
|
589
651
|
.type=${source?.type ?? ''}
|
|
652
|
+
.feedback=${this.proposalFeedback}
|
|
590
653
|
@proposed=${(e: CustomEvent) => this.receive(e.detail.proposal)}
|
|
591
654
|
></figure-ask>
|
|
592
655
|
${this.saveFailure ? html`<span save-failed title=${this.saveFailure}>${this.saveFailure}</span>` : ''}
|
|
@@ -603,6 +666,8 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
603
666
|
@figure-catalog=${(e: CustomEvent) => this.catalog(e.detail)}
|
|
604
667
|
@figure-released=${(e: CustomEvent) => this.released(e.detail.figure)}
|
|
605
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)}
|
|
606
671
|
@add-part=${(e: CustomEvent) => this.addPart(e.detail.primitive)}
|
|
607
672
|
@select-part=${(e: CustomEvent) => (this.selected = e.detail.index)}
|
|
608
673
|
@remove-part=${(e: CustomEvent) => this.removePart(e.detail.index)}
|
|
@@ -626,8 +691,8 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
626
691
|
|
|
627
692
|
<div middle>
|
|
628
693
|
${this.renderBar()}
|
|
629
|
-
${this.
|
|
630
|
-
${this.
|
|
694
|
+
${this.proposalSession ? this.renderDecision() : ''}
|
|
695
|
+
${this.proposalSession
|
|
631
696
|
? html`
|
|
632
697
|
<div lanes>
|
|
633
698
|
<div lane>
|
|
@@ -642,10 +707,11 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
642
707
|
<div lane candidate>
|
|
643
708
|
<h4>
|
|
644
709
|
${i18next.t('figure.label.proposed')}
|
|
645
|
-
${this.
|
|
710
|
+
${this.proposalSession.metrics
|
|
646
711
|
? html`<small>
|
|
647
|
-
${this.
|
|
648
|
-
${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}` : ''}
|
|
649
715
|
</small>`
|
|
650
716
|
: ''}
|
|
651
717
|
</h4>
|
|
@@ -711,7 +777,7 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
711
777
|
`
|
|
712
778
|
|
|
713
779
|
return html`<div modes>
|
|
714
|
-
${this.renderCollapse('left')}${this.
|
|
780
|
+
${this.renderCollapse('left')}${this.proposalSession ? '' : html`${one('edit')}${one('preview')}`}
|
|
715
781
|
<div spacer></div>
|
|
716
782
|
${this.renderCollapse('right')}
|
|
717
783
|
</div>`
|
|
@@ -746,16 +812,17 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
746
812
|
}
|
|
747
813
|
|
|
748
814
|
/**
|
|
749
|
-
* AI 후보를
|
|
815
|
+
* AI 후보를 실제 배치 크기에서도 본다.
|
|
750
816
|
*
|
|
751
|
-
*
|
|
752
|
-
*
|
|
817
|
+
* 편집 캔버스 한 장으로는 `repeat`·`fixed`·`anchor`가 맞는지 알 수 없다. 후보는 아직
|
|
818
|
+
* 정본이 아니므로 저장하지 않고, `figure-preview`가 같은 FigureSource를 기준 크기와
|
|
819
|
+
* 일곱 가지 확장 축에서 실제 FigureInstance로 세운다. 따라서 저작자는 "좋아 보이는
|
|
820
|
+
* 한 장"이 아니라 배치될 때도 붙어 있는지를 보고 수락한다.
|
|
753
821
|
*/
|
|
754
822
|
private renderProposed() {
|
|
755
|
-
if (!this.
|
|
823
|
+
if (!this.proposalSession) return ''
|
|
756
824
|
|
|
757
|
-
|
|
758
|
-
return html`<figure-canvas .board=${board} .parts=${parts} .viewOnly=${true}></figure-canvas>`
|
|
825
|
+
return html`<figure-preview .source=${this.proposalSession.source}></figure-preview>`
|
|
759
826
|
}
|
|
760
827
|
|
|
761
828
|
/**
|
|
@@ -773,22 +840,35 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
773
840
|
* 안 들고, 우리가 생각 못 한 조합을 막아 주기 때문이다.
|
|
774
841
|
*/
|
|
775
842
|
private renderDecision() {
|
|
776
|
-
if (!this.
|
|
843
|
+
if (!this.proposalSession) return ''
|
|
777
844
|
|
|
778
|
-
const changes = proposals.diffProposal(this.folded, this.
|
|
779
|
-
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)
|
|
780
847
|
const { errors, violations } = validate(picked)
|
|
781
848
|
|
|
782
849
|
return html`
|
|
783
850
|
<div decide>
|
|
784
|
-
<span>${i18next.t('figure.text.assistant-proposed-a-figure')}</span>
|
|
851
|
+
<span>${this.proposalSession.title || i18next.t('figure.text.assistant-proposed-a-figure')}</span>
|
|
785
852
|
<div spacer></div>
|
|
786
853
|
<button drop @click=${() => this.discard()}>${i18next.t('figure.button.discard')}</button>
|
|
787
|
-
<button take ?disabled=${errors.length > 0 || this.picked.size === 0} @click=${() => this.take()}>
|
|
788
|
-
${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 })}
|
|
789
856
|
</button>
|
|
790
857
|
</div>
|
|
791
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
|
+
|
|
792
872
|
${changes.length > 0
|
|
793
873
|
? html`
|
|
794
874
|
<div changes>
|
|
@@ -804,6 +884,25 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
804
884
|
`
|
|
805
885
|
}
|
|
806
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
|
+
|
|
807
906
|
/** 변경 하나. 무엇을 하겠다는 것인지 부호로 먼저 말한다. */
|
|
808
907
|
private renderChange(change: proposals.ProposalChange) {
|
|
809
908
|
const icon = {
|
|
@@ -816,7 +915,7 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
816
915
|
styleKit: 'palette'
|
|
817
916
|
}[change.kind]
|
|
818
917
|
|
|
819
|
-
const on = this.picked.has(change.key)
|
|
918
|
+
const on = this.proposalSession?.picked.has(change.key)
|
|
820
919
|
|
|
821
920
|
return html`
|
|
822
921
|
<button
|
|
@@ -832,10 +931,7 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
832
931
|
}
|
|
833
932
|
|
|
834
933
|
private toggle(key: string) {
|
|
835
|
-
|
|
836
|
-
if (next.has(key)) next.delete(key)
|
|
837
|
-
else next.add(key)
|
|
838
|
-
this.picked = next
|
|
934
|
+
if (this.proposalSession) this.proposalSession = proposalSessions.toggleProposalChange(this.proposalSession, key)
|
|
839
935
|
}
|
|
840
936
|
|
|
841
937
|
/**
|
|
@@ -844,22 +940,29 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
844
940
|
* 원본 데이터에 넣지 않는다. 나란히 두고 저작자가 고른다. AI 가 원본 데이터에 직접 쓰면
|
|
845
941
|
* 무엇이 자기 것이고 무엇이 기계 것인지 모르게 된다.
|
|
846
942
|
*/
|
|
847
|
-
private receive(proposal: {
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
this.
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
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()
|
|
863
966
|
}
|
|
864
967
|
|
|
865
968
|
/**
|
|
@@ -869,22 +972,45 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
869
972
|
* 하므로 그쪽이 자연스럽다. 받아들인 결과는 다시 씬 모델로 되돌려 편집을 이어간다.
|
|
870
973
|
*/
|
|
871
974
|
private take() {
|
|
872
|
-
if (!this.
|
|
975
|
+
if (!this.proposalSession) return
|
|
873
976
|
|
|
874
|
-
const taken = proposals.applyProposal(this.folded, this.
|
|
977
|
+
const taken = proposals.applyProposal(this.folded, this.proposalSession.source, this.proposalSession.picked)
|
|
875
978
|
const { board, parts } = fromFigureSource(taken)
|
|
876
979
|
|
|
877
980
|
this.board = board
|
|
878
981
|
this.parts = parts
|
|
879
982
|
this.selected = parts.length ? 0 : -1
|
|
880
983
|
this.dirty = true
|
|
881
|
-
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
|
+
)
|
|
882
1009
|
}
|
|
883
1010
|
|
|
884
|
-
private discard() {
|
|
885
|
-
this.
|
|
886
|
-
this.
|
|
887
|
-
this.picked = new Set()
|
|
1011
|
+
private discard(reportFeedback = true) {
|
|
1012
|
+
if (reportFeedback) this.reportProposalFeedback('discarded')
|
|
1013
|
+
this.proposalSession = undefined
|
|
888
1014
|
}
|
|
889
1015
|
|
|
890
1016
|
/**
|
package/client/types.ts
CHANGED
|
@@ -112,12 +112,31 @@ export interface FigureProposal {
|
|
|
112
112
|
triangles: number
|
|
113
113
|
/** 재질 묶음 = 인스턴스 하나가 무는 draw call. */
|
|
114
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
|
+
}
|
|
115
124
|
/** 형식은 맞으나 정책을 넘은 것. 막지 않는다 — 저작자가 정한다. */
|
|
116
125
|
violations: FigureFinding[]
|
|
117
126
|
/** 몇 번 만에 됐나. 진단용이다. */
|
|
118
127
|
attempts: number
|
|
119
128
|
}
|
|
120
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
|
+
|
|
121
140
|
/**
|
|
122
141
|
* 발행 판정 한 줄.
|
|
123
142
|
*
|
|
@@ -125,6 +144,18 @@ export interface FigureProposal {
|
|
|
125
144
|
* 발행 판정은 막는 것과 알리는 것을 함께 내므로 어느 쪽인지를 값이 스스로 말한다 — 코드를 보고
|
|
126
145
|
* 화면이 다시 분류하면 규칙이 두 벌이 된다.
|
|
127
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
|
+
|
|
128
159
|
export interface FigureGateFinding {
|
|
129
160
|
code: string
|
|
130
161
|
message: string
|
|
@@ -134,6 +165,8 @@ export interface FigureGateFinding {
|
|
|
134
165
|
how?: string
|
|
135
166
|
/** 갈라진 부품 이름들, 또는 정본 안의 경로. */
|
|
136
167
|
at?: string
|
|
168
|
+
/** Candidate edits; the UI must preview and explicitly apply these, never auto-save them. */
|
|
169
|
+
fixes?: FigureGateFix[]
|
|
137
170
|
blocking: boolean
|
|
138
171
|
}
|
|
139
172
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Figure, FigureFinding, FigureInspection, FigureListResult, FigurePatch, FigureProposal, FigureVersion, NewFigure } from '../types.js';
|
|
1
|
+
import type { Figure, FigureFinding, FigureInspection, FigureListResult, FigurePatch, FigureProposal, ProposalFeedback, FigureVersion, NewFigure } from '../types.js';
|
|
2
2
|
/**
|
|
3
3
|
* 목록을 가져온다.
|
|
4
4
|
*
|
|
@@ -46,6 +46,8 @@ export declare function proposeFigure(request: {
|
|
|
46
46
|
base?: string;
|
|
47
47
|
type?: string;
|
|
48
48
|
palette: string[];
|
|
49
|
+
refine?: boolean;
|
|
50
|
+
feedback?: ProposalFeedback[];
|
|
49
51
|
image?: File;
|
|
50
52
|
}): Promise<FigureProposal>;
|
|
51
53
|
/**
|
|
@@ -162,6 +162,18 @@ export async function proposeFigure(request) {
|
|
|
162
162
|
grade
|
|
163
163
|
triangles
|
|
164
164
|
groups
|
|
165
|
+
quality {
|
|
166
|
+
status
|
|
167
|
+
summary
|
|
168
|
+
findings {
|
|
169
|
+
dimension
|
|
170
|
+
code
|
|
171
|
+
message
|
|
172
|
+
}
|
|
173
|
+
visualRegions
|
|
174
|
+
visualCoverage
|
|
175
|
+
visualReadability
|
|
176
|
+
}
|
|
165
177
|
attempts
|
|
166
178
|
violations {
|
|
167
179
|
code
|
|
@@ -229,6 +241,15 @@ export async function inspectFigure(id) {
|
|
|
229
241
|
why
|
|
230
242
|
how
|
|
231
243
|
at
|
|
244
|
+
fixes {
|
|
245
|
+
findingWay
|
|
246
|
+
part
|
|
247
|
+
axis
|
|
248
|
+
from
|
|
249
|
+
to
|
|
250
|
+
resolved
|
|
251
|
+
safe
|
|
252
|
+
}
|
|
232
253
|
blocking
|
|
233
254
|
}
|
|
234
255
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../client/graphql/index.ts"],"names":[],"mappings":"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","sourcesContent":["import gql from 'graphql-tag'\nimport { client } from '@operato/graphql'\n\nimport type {\n Figure,\n FigureFinding,\n FigureInspection,\n FigureListResult,\n FigurePatch,\n FigureProposal,\n FigureVersion,\n NewFigure\n} from '../types.js'\n\n/**\n * 응답에서 값을 꺼낸다. **서버가 말한 사유를 그대로 올린다.**\n *\n * 전에는 `response.data.figures` 처럼 바로 꺼냈다. 서버가 오류를 돌려주면 `data` 가\n * 없으므로 `Cannot read properties of undefined (reading 'figures')` 가 났고, 화면에는\n * 그 문장이 그대로 떴다. 사용자는 자기가 무엇을 잘못했는지 알 수 없다 — 실제로 AI\n * 요청에서 그렇게 났다.\n *\n * 값이 `null` 인 것은 오류가 아니다. 지운 것을 물었을 때처럼 없는 것이 답인 경우가 있다.\n * 여기서 막는 것은 **`data` 자체가 없는 것**이다.\n */\nfunction unwrap<T>(response: { data?: Record<string, unknown>; errors?: readonly { message: string }[] }, field: string): T {\n const said = response.errors?.[0]?.message\n if (said) throw new Error(said)\n\n if (!response.data) {\n throw new Error(`서버가 ${field} 를 돌려주지 않았습니다.`)\n }\n\n return response.data[field] as T\n}\n\n\n/**\n * 목록에 필요한 필드만 가져온다 — `source` 는 무겁고 목록에서 쓰지 않는다.\n *\n * **`thumbnail` 도 여기 없다.** base64 문자열이라 목록 응답에 실으면 카탈로그를 열 때마다 그림\n * 전부가 다시 오고(표본 14개 227KB), data URL 은 브라우저가 캐시하지 못한다. 그림은 주소로\n * 부른다(`figure-thumbnail` 라우트). 대신 `thumbnailUpdatedAt` 을 받는다 — 그림이 있나 없나를\n * 그것으로 알고, 판을 가리는 값으로도 쓴다.\n */\nconst FIGURE_LIST_FIELDS = `\n id\n type\n name\n description\n category\n tags\n state\n version\n score\n triangles\n groups\n thumbnailUpdatedAt\n updatedAt\n updater { id name }\n`\n\n/**\n * 목록을 가져온다.\n *\n * 걸러 보기·정렬·쪽 나누기를 **그대로 넘긴다.** 화면이 `search`·`state` 같은 이름을 따로 만들어\n * filter 로 옮기던 것을 그만두었다 — 그 이름들은 격자(ox-grist)가 이미 컬럼 설정에서 만들어 주고,\n * 중간에 한 벌 더 두면 격자가 아는 조건과 서버에 가는 조건이 어긋난다.\n */\nexport async function fetchFigureList(params: {\n page?: number\n limit?: number\n filters?: unknown[]\n sortings?: { name: string; desc?: boolean }[]\n}): Promise<FigureListResult> {\n const response = await client.query({\n query: gql`\n query ($filters: [Filter!], $pagination: Pagination, $sortings: [Sorting!]) {\n figures(filters: $filters, pagination: $pagination, sortings: $sortings) {\n items { ${FIGURE_LIST_FIELDS} }\n total\n }\n }\n `,\n variables: {\n filters: params.filters ?? [],\n pagination: { page: params.page ?? 1, limit: params.limit ?? 30 },\n sortings: params.sortings?.length ? params.sortings : [{ name: 'updatedAt', desc: true }]\n },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<FigureListResult>(response, 'figures')\n}\n\n/** 저작면에 필요한 전부 — `source` 를 포함한다. */\nexport async function fetchFigure(id: string): Promise<Figure> {\n const response = await client.query({\n query: gql`\n query ($id: String!) {\n figure(id: $id) {\n ${FIGURE_LIST_FIELDS}\n source\n properties\n }\n }\n `,\n variables: { id },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<Figure>(response, 'figure')\n}\n\n/**\n * 이미 쓰이고 있는 타입 이름.\n *\n * `type` 은 만든 뒤에 고칠 수 없으므로 **만들기 전에** 알려 줘야 한다. 저장을 눌러\n * 거절당하고 나서 아는 것은 늦다.\n */\nexport async function fetchFigureTypeNames(): Promise<string[]> {\n const response = await client.query({\n query: gql`\n query {\n figureTypeNames\n }\n `,\n fetchPolicy: 'network-only'\n })\n\n return unwrap<string[] | null>(response, 'figureTypeNames') ?? []\n}\n\nexport interface SaveResult {\n figure: Figure\n /** 막지 않는 발견. 저장은 됐지만 화면이 보여 줘야 한다. */\n violations: FigureFinding[]\n}\n\nexport async function createFigure(figure: NewFigure): Promise<SaveResult> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($figure: NewFigure!) {\n createFigure(figure: $figure) {\n figure { ${FIGURE_LIST_FIELDS} source }\n violations { code message at }\n }\n }\n `,\n variables: { figure }\n })\n\n return unwrap<SaveResult>(response, 'createFigure')\n}\n\nexport async function updateFigure(id: string, patch: FigurePatch): Promise<SaveResult> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($id: String!, $patch: FigurePatch!) {\n updateFigure(id: $id, patch: $patch) {\n figure { ${FIGURE_LIST_FIELDS} source }\n violations { code message at }\n }\n }\n `,\n variables: { id, patch }\n })\n\n return unwrap<SaveResult>(response, 'updateFigure')\n}\n\nexport async function deleteFigure(id: string): Promise<boolean> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($id: String!) {\n deleteFigure(id: $id)\n }\n `,\n variables: { id }\n })\n\n return unwrap<boolean>(response, 'deleteFigure')\n}\n\n/**\n * 저작 보조에게 후보를 시킨다.\n *\n * 팔레트를 **클라이언트가 보낸다.** 서버는 팔레트를 모른다 — things-scene 이 갖고\n * 있는데 그 패키지의 node 조건은 팔레트가 없는 번들로 간다. 그리는 쪽이 어떤 토큰을\n * 풀 수 있는지 아는 유일한 자리이므로 그쪽이 보낸다.\n *\n * 돌아오는 것은 **후보**다. 저장되지 않았다 — 저작자가 받아야 정본이 된다.\n */\nexport async function proposeFigure(request: {\n prompt: string\n base?: string\n type?: string\n palette: string[]\n image?: File\n}): Promise<FigureProposal> {\n const response = await client.mutate({\n mutation: gql`\n mutation ProposeFigure($request: ProposeRequest!) {\n proposeFigure(request: $request) {\n source\n score\n grade\n triangles\n groups\n attempts\n violations {\n code\n message\n }\n }\n }\n `,\n variables: { request }\n })\n\n return unwrap<FigureProposal>(response, 'proposeFigure')\n}\n\n/**\n * 발행한다 — 판 번호가 오르고, 그 순간이 판본으로 남는다.\n *\n * 이미 발행된 것을 다시 발행하면 서버가 거절한다. 고쳐서 다시 내려면 저장이 먼저이고, 저장은\n * 발행을 푼다(초안으로 돌아온다).\n */\nexport async function releaseFigure(id: string, comment?: string): Promise<Figure> {\n const response = await client.mutate({\n mutation: gql`\n mutation ReleaseFigure($id: String!, $comment: String) {\n releaseFigure(id: $id, comment: $comment) {\n id\n version\n state\n updatedAt\n }\n }\n `,\n variables: { id, comment }\n })\n\n return unwrap<Figure>(response, 'releaseFigure')\n}\n\n/** 옛 판을 초안으로 되살린다. 정본·속성·그림이 돌아오고 이름·설명은 그대로다. */\nexport async function revertFigureVersion(id: string, version: number): Promise<SaveResult> {\n const response = await client.mutate({\n mutation: gql`\n mutation RevertFigureVersion($id: String!, $version: Float!) {\n revertFigureVersion(id: $id, version: $version) {\n figure { ${FIGURE_LIST_FIELDS} source }\n violations { code message at }\n }\n }\n `,\n variables: { id, version }\n })\n\n return unwrap<SaveResult>(response, 'revertFigureVersion')\n}\n\n/**\n * 발행해도 되나 — **누르기 전에** 묻는다.\n *\n * 막는 것은 서버의 `releaseFigure` 이고 이것은 이유를 미리 보여 주기 위한 것이다. 허가증이\n * 아니므로 이 답이 통과라 해도 발행은 다시 판정을 받는다.\n */\nexport async function inspectFigure(id: string): Promise<FigureInspection> {\n const response = await client.query({\n query: gql`\n query InspectFigure($id: String!) {\n inspectFigure(id: $id) {\n blocked\n findings {\n code\n message\n why\n how\n at\n blocking\n }\n }\n }\n `,\n variables: { id },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<FigureInspection>(response, 'inspectFigure')\n}\n\n/** 발행된 판들 — 최근 것부터 열 개. */\nexport async function fetchFigureVersions(id: string): Promise<FigureVersion[]> {\n const response = await client.query({\n query: gql`\n query FigureVersions($id: String!) {\n figureVersions(id: $id) {\n version\n comment\n state\n updatedAt\n updater { id name }\n score\n triangles\n groups\n }\n }\n `,\n variables: { id },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<FigureVersion[]>(response, 'figureVersions') ?? []\n}\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../client/graphql/index.ts"],"names":[],"mappings":"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","sourcesContent":["import gql from 'graphql-tag'\nimport { client } from '@operato/graphql'\n\nimport type {\n Figure,\n FigureFinding,\n FigureInspection,\n FigureListResult,\n FigurePatch,\n FigureProposal,\n ProposalFeedback,\n FigureVersion,\n NewFigure\n} from '../types.js'\n\n/**\n * 응답에서 값을 꺼낸다. **서버가 말한 사유를 그대로 올린다.**\n *\n * 전에는 `response.data.figures` 처럼 바로 꺼냈다. 서버가 오류를 돌려주면 `data` 가\n * 없으므로 `Cannot read properties of undefined (reading 'figures')` 가 났고, 화면에는\n * 그 문장이 그대로 떴다. 사용자는 자기가 무엇을 잘못했는지 알 수 없다 — 실제로 AI\n * 요청에서 그렇게 났다.\n *\n * 값이 `null` 인 것은 오류가 아니다. 지운 것을 물었을 때처럼 없는 것이 답인 경우가 있다.\n * 여기서 막는 것은 **`data` 자체가 없는 것**이다.\n */\nfunction unwrap<T>(response: { data?: Record<string, unknown>; errors?: readonly { message: string }[] }, field: string): T {\n const said = response.errors?.[0]?.message\n if (said) throw new Error(said)\n\n if (!response.data) {\n throw new Error(`서버가 ${field} 를 돌려주지 않았습니다.`)\n }\n\n return response.data[field] as T\n}\n\n\n/**\n * 목록에 필요한 필드만 가져온다 — `source` 는 무겁고 목록에서 쓰지 않는다.\n *\n * **`thumbnail` 도 여기 없다.** base64 문자열이라 목록 응답에 실으면 카탈로그를 열 때마다 그림\n * 전부가 다시 오고(표본 14개 227KB), data URL 은 브라우저가 캐시하지 못한다. 그림은 주소로\n * 부른다(`figure-thumbnail` 라우트). 대신 `thumbnailUpdatedAt` 을 받는다 — 그림이 있나 없나를\n * 그것으로 알고, 판을 가리는 값으로도 쓴다.\n */\nconst FIGURE_LIST_FIELDS = `\n id\n type\n name\n description\n category\n tags\n state\n version\n score\n triangles\n groups\n thumbnailUpdatedAt\n updatedAt\n updater { id name }\n`\n\n/**\n * 목록을 가져온다.\n *\n * 걸러 보기·정렬·쪽 나누기를 **그대로 넘긴다.** 화면이 `search`·`state` 같은 이름을 따로 만들어\n * filter 로 옮기던 것을 그만두었다 — 그 이름들은 격자(ox-grist)가 이미 컬럼 설정에서 만들어 주고,\n * 중간에 한 벌 더 두면 격자가 아는 조건과 서버에 가는 조건이 어긋난다.\n */\nexport async function fetchFigureList(params: {\n page?: number\n limit?: number\n filters?: unknown[]\n sortings?: { name: string; desc?: boolean }[]\n}): Promise<FigureListResult> {\n const response = await client.query({\n query: gql`\n query ($filters: [Filter!], $pagination: Pagination, $sortings: [Sorting!]) {\n figures(filters: $filters, pagination: $pagination, sortings: $sortings) {\n items { ${FIGURE_LIST_FIELDS} }\n total\n }\n }\n `,\n variables: {\n filters: params.filters ?? [],\n pagination: { page: params.page ?? 1, limit: params.limit ?? 30 },\n sortings: params.sortings?.length ? params.sortings : [{ name: 'updatedAt', desc: true }]\n },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<FigureListResult>(response, 'figures')\n}\n\n/** 저작면에 필요한 전부 — `source` 를 포함한다. */\nexport async function fetchFigure(id: string): Promise<Figure> {\n const response = await client.query({\n query: gql`\n query ($id: String!) {\n figure(id: $id) {\n ${FIGURE_LIST_FIELDS}\n source\n properties\n }\n }\n `,\n variables: { id },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<Figure>(response, 'figure')\n}\n\n/**\n * 이미 쓰이고 있는 타입 이름.\n *\n * `type` 은 만든 뒤에 고칠 수 없으므로 **만들기 전에** 알려 줘야 한다. 저장을 눌러\n * 거절당하고 나서 아는 것은 늦다.\n */\nexport async function fetchFigureTypeNames(): Promise<string[]> {\n const response = await client.query({\n query: gql`\n query {\n figureTypeNames\n }\n `,\n fetchPolicy: 'network-only'\n })\n\n return unwrap<string[] | null>(response, 'figureTypeNames') ?? []\n}\n\nexport interface SaveResult {\n figure: Figure\n /** 막지 않는 발견. 저장은 됐지만 화면이 보여 줘야 한다. */\n violations: FigureFinding[]\n}\n\nexport async function createFigure(figure: NewFigure): Promise<SaveResult> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($figure: NewFigure!) {\n createFigure(figure: $figure) {\n figure { ${FIGURE_LIST_FIELDS} source }\n violations { code message at }\n }\n }\n `,\n variables: { figure }\n })\n\n return unwrap<SaveResult>(response, 'createFigure')\n}\n\nexport async function updateFigure(id: string, patch: FigurePatch): Promise<SaveResult> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($id: String!, $patch: FigurePatch!) {\n updateFigure(id: $id, patch: $patch) {\n figure { ${FIGURE_LIST_FIELDS} source }\n violations { code message at }\n }\n }\n `,\n variables: { id, patch }\n })\n\n return unwrap<SaveResult>(response, 'updateFigure')\n}\n\nexport async function deleteFigure(id: string): Promise<boolean> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($id: String!) {\n deleteFigure(id: $id)\n }\n `,\n variables: { id }\n })\n\n return unwrap<boolean>(response, 'deleteFigure')\n}\n\n/**\n * 저작 보조에게 후보를 시킨다.\n *\n * 팔레트를 **클라이언트가 보낸다.** 서버는 팔레트를 모른다 — things-scene 이 갖고\n * 있는데 그 패키지의 node 조건은 팔레트가 없는 번들로 간다. 그리는 쪽이 어떤 토큰을\n * 풀 수 있는지 아는 유일한 자리이므로 그쪽이 보낸다.\n *\n * 돌아오는 것은 **후보**다. 저장되지 않았다 — 저작자가 받아야 정본이 된다.\n */\nexport async function proposeFigure(request: {\n prompt: string\n base?: string\n type?: string\n palette: string[]\n refine?: boolean\n feedback?: ProposalFeedback[]\n image?: File\n}): Promise<FigureProposal> {\n const response = await client.mutate({\n mutation: gql`\n mutation ProposeFigure($request: ProposeRequest!) {\n proposeFigure(request: $request) {\n source\n score\n grade\n triangles\n groups\n quality {\n status\n summary\n findings {\n dimension\n code\n message\n }\n visualRegions\n visualCoverage\n visualReadability\n }\n attempts\n violations {\n code\n message\n }\n }\n }\n `,\n variables: { request }\n })\n\n return unwrap<FigureProposal>(response, 'proposeFigure')\n}\n\n/**\n * 발행한다 — 판 번호가 오르고, 그 순간이 판본으로 남는다.\n *\n * 이미 발행된 것을 다시 발행하면 서버가 거절한다. 고쳐서 다시 내려면 저장이 먼저이고, 저장은\n * 발행을 푼다(초안으로 돌아온다).\n */\nexport async function releaseFigure(id: string, comment?: string): Promise<Figure> {\n const response = await client.mutate({\n mutation: gql`\n mutation ReleaseFigure($id: String!, $comment: String) {\n releaseFigure(id: $id, comment: $comment) {\n id\n version\n state\n updatedAt\n }\n }\n `,\n variables: { id, comment }\n })\n\n return unwrap<Figure>(response, 'releaseFigure')\n}\n\n/** 옛 판을 초안으로 되살린다. 정본·속성·그림이 돌아오고 이름·설명은 그대로다. */\nexport async function revertFigureVersion(id: string, version: number): Promise<SaveResult> {\n const response = await client.mutate({\n mutation: gql`\n mutation RevertFigureVersion($id: String!, $version: Float!) {\n revertFigureVersion(id: $id, version: $version) {\n figure { ${FIGURE_LIST_FIELDS} source }\n violations { code message at }\n }\n }\n `,\n variables: { id, version }\n })\n\n return unwrap<SaveResult>(response, 'revertFigureVersion')\n}\n\n/**\n * 발행해도 되나 — **누르기 전에** 묻는다.\n *\n * 막는 것은 서버의 `releaseFigure` 이고 이것은 이유를 미리 보여 주기 위한 것이다. 허가증이\n * 아니므로 이 답이 통과라 해도 발행은 다시 판정을 받는다.\n */\nexport async function inspectFigure(id: string): Promise<FigureInspection> {\n const response = await client.query({\n query: gql`\n query InspectFigure($id: String!) {\n inspectFigure(id: $id) {\n blocked\n findings {\n code\n message\n why\n how\n at\n fixes {\n findingWay\n part\n axis\n from\n to\n resolved\n safe\n }\n blocking\n }\n }\n }\n `,\n variables: { id },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<FigureInspection>(response, 'inspectFigure')\n}\n\n/** 발행된 판들 — 최근 것부터 열 개. */\nexport async function fetchFigureVersions(id: string): Promise<FigureVersion[]> {\n const response = await client.query({\n query: gql`\n query FigureVersions($id: String!) {\n figureVersions(id: $id) {\n version\n comment\n state\n updatedAt\n updater { id name }\n score\n triangles\n groups\n }\n }\n `,\n variables: { id },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<FigureVersion[]>(response, 'figureVersions') ?? []\n}\n"]}
|
|
@@ -7,6 +7,7 @@ export declare class FigureAnimations extends FigureAnimations_base {
|
|
|
7
7
|
parts: PartModel[];
|
|
8
8
|
/** 펼쳐 둔 clip. 이름이 아니라 자리로 잡는다 — 이름은 편집 중에 바뀐다. */
|
|
9
9
|
private open;
|
|
10
|
+
private recipeTarget;
|
|
10
11
|
private get clips();
|
|
11
12
|
/** 부품 이름 — 채널이 이것을 가리킨다. */
|
|
12
13
|
private get partNames();
|
|
@@ -24,7 +25,7 @@ export declare class FigureAnimations extends FigureAnimations_base {
|
|
|
24
25
|
* 「clip 을 추가하세요」라고만 쓰면 처음 여는 사람은 clip 이 무엇인지 모른 채 단추를 누른다.
|
|
25
26
|
* 그래서 무엇을 만들 수 있는지 예를 들고, 그것이 어떻게 움직이는지(사실이 들어온다)를 적는다.
|
|
26
27
|
*/
|
|
27
|
-
private
|
|
28
|
+
private renderRecipes;
|
|
28
29
|
private renderClips;
|
|
29
30
|
private renderClip;
|
|
30
31
|
private renderChannels;
|
|
@@ -70,6 +71,8 @@ export declare class FigureAnimations extends FigureAnimations_base {
|
|
|
70
71
|
private patchClip;
|
|
71
72
|
private patchChannel;
|
|
72
73
|
private addClip;
|
|
74
|
+
/** Recipe creates a valid, editable two-pose clip; it never saves or publishes the Figure. */
|
|
75
|
+
private addRecipe;
|
|
73
76
|
private renameClip;
|
|
74
77
|
private setDrive;
|
|
75
78
|
private removeClip;
|