@things-factory/figure-ui 10.1.10 → 10.1.13
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/modeller/figure-ai-target.ts +11 -0
- package/client/modeller/figure-ask.ts +8 -5
- package/client/modeller/new-figure-entry.ts +4 -0
- package/client/pages/figure-modeller-page.ts +62 -24
- package/dist-client/modeller/figure-ai-target.d.ts +8 -0
- package/dist-client/modeller/figure-ai-target.js +12 -0
- package/dist-client/modeller/figure-ai-target.js.map +1 -0
- package/dist-client/modeller/figure-ask.d.ts +1 -0
- package/dist-client/modeller/figure-ask.js +12 -5
- package/dist-client/modeller/figure-ask.js.map +1 -1
- package/dist-client/modeller/new-figure-entry.d.ts +2 -0
- package/dist-client/modeller/new-figure-entry.js +5 -0
- package/dist-client/modeller/new-figure-entry.js.map +1 -0
- package/dist-client/pages/figure-modeller-page.d.ts +5 -0
- package/dist-client/pages/figure-modeller-page.js +62 -22
- package/dist-client/pages/figure-modeller-page.js.map +1 -1
- package/dist-client/route.d.ts +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/test/ai-proposal-contract.test.ts +5 -3
- package/test/ai-target.test.ts +25 -0
- package/test/new-figure-entry.test.ts +15 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Explicit AI entry survives lazy creation of the app-level dock. */
|
|
2
|
+
let pending: { figureId?: string; source?: unknown } | undefined
|
|
3
|
+
export function requestFigureAI(target: { figureId?: string; source?: unknown }) {
|
|
4
|
+
pending = structuredClone(target)
|
|
5
|
+
window.dispatchEvent(new CustomEvent('figure-ai-target'))
|
|
6
|
+
}
|
|
7
|
+
export function takeFigureAITarget() {
|
|
8
|
+
const target = pending
|
|
9
|
+
pending = undefined
|
|
10
|
+
return target
|
|
11
|
+
}
|
|
@@ -58,6 +58,7 @@ export class FigureAsk extends localize(i18next)(LitElement) {
|
|
|
58
58
|
|
|
59
59
|
form {
|
|
60
60
|
display: flex;
|
|
61
|
+
flex-wrap: wrap;
|
|
61
62
|
flex: 1;
|
|
62
63
|
min-width: 0;
|
|
63
64
|
align-items: center;
|
|
@@ -177,6 +178,7 @@ export class FigureAsk extends localize(i18next)(LitElement) {
|
|
|
177
178
|
@property({ type: String }) type = ''
|
|
178
179
|
/** 이 모델러 세션에서 사람이 명시적으로 남긴 이전 후보 반응. */
|
|
179
180
|
@property({ type: Array }) feedback: ProposalFeedback[] = []
|
|
181
|
+
@property({ type: Boolean }) imageOnly = false
|
|
180
182
|
|
|
181
183
|
@state() private asking = false
|
|
182
184
|
@state() private failure = ''
|
|
@@ -186,7 +188,7 @@ export class FigureAsk extends localize(i18next)(LitElement) {
|
|
|
186
188
|
const revising = !!this.source
|
|
187
189
|
|
|
188
190
|
return html`
|
|
189
|
-
<form @submit=${(e: Event) => this.submit(e)}>
|
|
191
|
+
<form title="이미지의 형태와 비율을 참고한 후보를 만듭니다. 저장은 하지 않습니다." @submit=${(e: Event) => this.submit(e)}>
|
|
190
192
|
<md-icon lead>auto_awesome</md-icon>
|
|
191
193
|
<span mode>${i18next.t(revising ? 'figure.text.ask-to-revise' : 'figure.text.ask-to-create')}</span>
|
|
192
194
|
<input
|
|
@@ -201,7 +203,7 @@ export class FigureAsk extends localize(i18next)(LitElement) {
|
|
|
201
203
|
: i18next.t('figure.button.attach-a-picture')}
|
|
202
204
|
<input type="file" accept="image/*" @change=${(e: Event) => this.pick(e)} />
|
|
203
205
|
</label>
|
|
204
|
-
<button go ?disabled=${this.asking}>
|
|
206
|
+
<button go ?disabled=${this.asking || (this.imageOnly && !this.picture)}>
|
|
205
207
|
${this.asking ? i18next.t('figure.text.asking') : i18next.t('figure.button.ask')}
|
|
206
208
|
</button>
|
|
207
209
|
</form>
|
|
@@ -227,17 +229,18 @@ export class FigureAsk extends localize(i18next)(LitElement) {
|
|
|
227
229
|
|
|
228
230
|
const input = this.renderRoot.querySelector('input[ask]') as HTMLInputElement
|
|
229
231
|
const prompt = input?.value?.trim()
|
|
230
|
-
if (!prompt || this.asking) {
|
|
232
|
+
if (!prompt || this.asking || (this.imageOnly && !this.picture)) {
|
|
231
233
|
return
|
|
232
234
|
}
|
|
233
235
|
|
|
234
236
|
this.asking = true
|
|
235
237
|
this.failure = ''
|
|
238
|
+
const baseSource = this.source ? JSON.stringify(this.source) : undefined
|
|
236
239
|
|
|
237
240
|
try {
|
|
238
241
|
const proposal = await proposeFigure({
|
|
239
242
|
prompt,
|
|
240
|
-
base:
|
|
243
|
+
base: baseSource,
|
|
241
244
|
type: this.source ? undefined : this.type,
|
|
242
245
|
// 그리는 쪽이 풀 수 있는 토큰만 보낸다. 서버는 이 목록 밖의 색을 거절한다.
|
|
243
246
|
palette: Object.keys(activePalette() as Record<string, string>),
|
|
@@ -248,7 +251,7 @@ export class FigureAsk extends localize(i18next)(LitElement) {
|
|
|
248
251
|
})
|
|
249
252
|
|
|
250
253
|
this.dispatchEvent(
|
|
251
|
-
new CustomEvent('proposed', { detail: { proposal }, bubbles: true, composed: true })
|
|
254
|
+
new CustomEvent('proposed', { detail: { proposal, baseSource }, bubbles: true, composed: true })
|
|
252
255
|
)
|
|
253
256
|
input.value = ''
|
|
254
257
|
this.picture = undefined
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** A route entry starts a new document; ordinary updates must preserve edits. */
|
|
2
|
+
export function shouldStartNewFigure(changes: Record<string, unknown>, hasFigure: boolean, savedId?: string): boolean {
|
|
3
|
+
return !hasFigure || !!savedId || changes.active === true || Object.prototype.hasOwnProperty.call(changes, 'resourceId')
|
|
4
|
+
}
|
|
@@ -4,6 +4,7 @@ import '../modeller/figure-ask.js'
|
|
|
4
4
|
import '../modeller/figure-canvas.js'
|
|
5
5
|
import { captureThumbnail } from '../modeller/figure-thumbnail.js'
|
|
6
6
|
import '../modeller/figure-preview.js'
|
|
7
|
+
import { shouldStartNewFigure } from '../modeller/new-figure-entry.js'
|
|
7
8
|
import '../modeller/figure-parts.js'
|
|
8
9
|
import '../modeller/figure-inspector.js'
|
|
9
10
|
|
|
@@ -12,6 +13,8 @@ import { customElement, state } from 'lit/decorators.js'
|
|
|
12
13
|
|
|
13
14
|
import { i18next, localize } from '@operato/i18n'
|
|
14
15
|
import { navigate, PageView } from '@operato/shell'
|
|
16
|
+
import { openOverlay } from '@operato/layout'
|
|
17
|
+
import { requestFigureAI } from '../modeller/figure-ai-target.js'
|
|
15
18
|
import { validate } from '@hatiolab/figure-model'
|
|
16
19
|
import type { DetailLevel, FigureSource, PrimitiveKind } from '@hatiolab/figure-model'
|
|
17
20
|
|
|
@@ -199,9 +202,15 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
199
202
|
padding: var(--spacing-medium, 8px) var(--spacing-large, 12px);
|
|
200
203
|
border-bottom: 1px solid var(--md-sys-color-outline-variant);
|
|
201
204
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
+
button[ai-support] {
|
|
206
|
+
height: 28px;
|
|
207
|
+
padding: 0 12px;
|
|
208
|
+
border: 1px solid var(--md-sys-color-outline-variant);
|
|
209
|
+
border-radius: 6px;
|
|
210
|
+
background: var(--md-sys-color-primary-container);
|
|
211
|
+
color: var(--md-sys-color-on-primary-container);
|
|
212
|
+
font: inherit;
|
|
213
|
+
cursor: pointer;
|
|
205
214
|
}
|
|
206
215
|
input[name] {
|
|
207
216
|
flex: none;
|
|
@@ -609,7 +618,33 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
609
618
|
}
|
|
610
619
|
}
|
|
611
620
|
|
|
621
|
+
private announceAiContext = () => {
|
|
622
|
+
if (!this.active) return
|
|
623
|
+
window.dispatchEvent(new CustomEvent('figure-ai-context', {
|
|
624
|
+
detail: { figureId: this.figure?.id, source: this.folded }
|
|
625
|
+
}))
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
private acceptDockProposal = (event: Event) => {
|
|
629
|
+
const detail = (event as CustomEvent).detail
|
|
630
|
+
if (!this.active || (detail?.figureId ?? '') !== (this.figure?.id ?? '')) return
|
|
631
|
+
if (detail?.draftType && detail.draftType !== this.folded?.type) return
|
|
632
|
+
if (detail?.baseSource && detail.baseSource !== JSON.stringify(this.folded)) return
|
|
633
|
+
this.receive(detail.proposal)
|
|
634
|
+
detail.accepted = true
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
private proposalBaseSource?: string
|
|
638
|
+
|
|
639
|
+
connectedCallback() {
|
|
640
|
+
super.connectedCallback()
|
|
641
|
+
window.addEventListener('figure-ai-context-request', this.announceAiContext)
|
|
642
|
+
window.addEventListener('figure-ai-review', this.acceptDockProposal)
|
|
643
|
+
}
|
|
644
|
+
|
|
612
645
|
disconnectedCallback() {
|
|
646
|
+
window.removeEventListener('figure-ai-context-request', this.announceAiContext)
|
|
647
|
+
window.removeEventListener('figure-ai-review', this.acceptDockProposal)
|
|
613
648
|
window.dispatchEvent(new CustomEvent('figure-ai-context', { detail: undefined }))
|
|
614
649
|
super.disconnectedCallback()
|
|
615
650
|
}
|
|
@@ -646,12 +681,10 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
646
681
|
말로 시키는 줄이 머리줄의 남는 폭을 쓴다. 따로 두면 화면 위가 두 줄이 되고, 그 위 줄은
|
|
647
682
|
이름을 페이지 제목과 겹쳐 적는다.
|
|
648
683
|
-->
|
|
649
|
-
<
|
|
650
|
-
.source
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
@proposed=${(e: CustomEvent) => this.receive(e.detail.proposal)}
|
|
654
|
-
></figure-ask>
|
|
684
|
+
<button ai-support @click=${() => {
|
|
685
|
+
requestFigureAI({ figureId: this.figure?.id, source: this.folded })
|
|
686
|
+
openOverlay('figure-ai-dock', { backdrop: false })
|
|
687
|
+
}}>AI 지원</button>
|
|
655
688
|
${this.saveFailure ? html`<span save-failed title=${this.saveFailure}>${this.saveFailure}</span>` : ''}
|
|
656
689
|
<button save ?disabled=${!this.canSave} @click=${() => this.save()}>
|
|
657
690
|
${this.saving ? i18next.t('figure.text.saving-figure') : i18next.t('figure.button.save')}
|
|
@@ -842,6 +875,8 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
842
875
|
private renderDecision() {
|
|
843
876
|
if (!this.proposalSession) return ''
|
|
844
877
|
|
|
878
|
+
const stale = this.proposalBaseSource !== undefined && this.proposalBaseSource !== JSON.stringify(this.folded)
|
|
879
|
+
|
|
845
880
|
const changes = proposals.diffProposal(this.folded, this.proposalSession.source)
|
|
846
881
|
const picked = proposals.applyProposal(this.folded, this.proposalSession.source, this.proposalSession.picked)
|
|
847
882
|
const { errors, violations } = validate(picked)
|
|
@@ -851,7 +886,8 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
851
886
|
<span>${this.proposalSession.title || i18next.t('figure.text.assistant-proposed-a-figure')}</span>
|
|
852
887
|
<div spacer></div>
|
|
853
888
|
<button drop @click=${() => this.discard()}>${i18next.t('figure.button.discard')}</button>
|
|
854
|
-
|
|
889
|
+
${stale ? html`<span role="alert">원본이 변경되었습니다. 후보를 취소하고 AI 지원에서 다시 요청하세요.</span>` : ''}
|
|
890
|
+
<button take ?disabled=${stale || errors.length > 0 || this.proposalSession.picked.size === 0} @click=${() => this.take()}>
|
|
855
891
|
${i18next.t('figure.button.take-n-changes', { n: this.proposalSession.picked.size })}
|
|
856
892
|
</button>
|
|
857
893
|
</div>
|
|
@@ -941,6 +977,7 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
941
977
|
* 무엇이 자기 것이고 무엇이 기계 것인지 모르게 된다.
|
|
942
978
|
*/
|
|
943
979
|
private receive(proposal: FigureProposal) {
|
|
980
|
+
this.proposalBaseSource = JSON.stringify(this.folded)
|
|
944
981
|
this.proposalSession = proposalSessions.openAiProposal(this.folded, proposal)
|
|
945
982
|
}
|
|
946
983
|
|
|
@@ -951,6 +988,7 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
951
988
|
*/
|
|
952
989
|
private previewSizingFix(fix: FigureGateFix) {
|
|
953
990
|
if (!this.folded) return
|
|
991
|
+
this.proposalBaseSource = JSON.stringify(this.folded)
|
|
954
992
|
this.proposalSession = proposalSessions.openSizingFix(this.folded, fix)
|
|
955
993
|
}
|
|
956
994
|
|
|
@@ -973,6 +1011,7 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
973
1011
|
*/
|
|
974
1012
|
private take() {
|
|
975
1013
|
if (!this.proposalSession) return
|
|
1014
|
+
if (this.proposalBaseSource !== undefined && this.proposalBaseSource !== JSON.stringify(this.folded)) return
|
|
976
1015
|
|
|
977
1016
|
const taken = proposals.applyProposal(this.folded, this.proposalSession.source, this.proposalSession.picked)
|
|
978
1017
|
const { board, parts } = fromFigureSource(taken)
|
|
@@ -1011,6 +1050,7 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
1011
1050
|
private discard(reportFeedback = true) {
|
|
1012
1051
|
if (reportFeedback) this.reportProposalFeedback('discarded')
|
|
1013
1052
|
this.proposalSession = undefined
|
|
1053
|
+
this.proposalBaseSource = undefined
|
|
1014
1054
|
}
|
|
1015
1055
|
|
|
1016
1056
|
/**
|
|
@@ -1037,6 +1077,7 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
1037
1077
|
|
|
1038
1078
|
async pageUpdated(changes: Record<string, unknown>, lifecycle: { resourceId?: string }) {
|
|
1039
1079
|
if (!this.active) {
|
|
1080
|
+
++this.loadGeneration
|
|
1040
1081
|
return
|
|
1041
1082
|
}
|
|
1042
1083
|
|
|
@@ -1061,28 +1102,20 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
1061
1102
|
들고 있는 것이 **저장된 도형이면**(id 가 있으면) 새로 시작한다. 아직 저장 안 한 새 도형을
|
|
1062
1103
|
들고 있으면 그대로 둔다 — 페이지를 다시 들렀다고 저작 중인 것을 버리지 않는다.
|
|
1063
1104
|
*/
|
|
1064
|
-
if (
|
|
1105
|
+
if (shouldStartNewFigure(changes, !!this.figure, this.figure?.id)) {
|
|
1065
1106
|
this.startNew()
|
|
1066
1107
|
return
|
|
1067
1108
|
}
|
|
1068
|
-
|
|
1069
|
-
if (this.figure.id) {
|
|
1070
|
-
/*
|
|
1071
|
-
고치던 것이 있으면 묻는다. 저작한 것을 말없이 버리지 않는다 — 되돌릴 수 없다.
|
|
1072
|
-
그만두면 보고 있던 도형의 주소로 되돌려 화면과 주소가 어긋나지 않게 한다.
|
|
1073
|
-
*/
|
|
1074
|
-
if (this.dirty && !confirm(i18next.t('figure.text.discard-changes-and-start-new'))) {
|
|
1075
|
-
navigate(`figure-modeller/${this.figure.id}`, true)
|
|
1076
|
-
return
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
|
-
this.startNew()
|
|
1080
|
-
}
|
|
1081
1109
|
}
|
|
1082
1110
|
|
|
1111
|
+
private loadGeneration = 0
|
|
1083
1112
|
private async load(id: string) {
|
|
1113
|
+
const generation = ++this.loadGeneration
|
|
1084
1114
|
const figure = await fetchFigure(id)
|
|
1115
|
+
if (generation !== this.loadGeneration || !this.active) return
|
|
1085
1116
|
|
|
1117
|
+
this.proposalSession = undefined
|
|
1118
|
+
this.proposalBaseSource = undefined
|
|
1086
1119
|
this.figure = figure
|
|
1087
1120
|
|
|
1088
1121
|
/*
|
|
@@ -1119,6 +1152,11 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
1119
1152
|
}
|
|
1120
1153
|
|
|
1121
1154
|
private startNew() {
|
|
1155
|
+
++this.loadGeneration
|
|
1156
|
+
this.proposalSession = undefined
|
|
1157
|
+
this.proposalBaseSource = undefined
|
|
1158
|
+
this.proposalFeedback = []
|
|
1159
|
+
this.mode = 'edit'
|
|
1122
1160
|
// 타입 이름을 사람이 정하기 전까지는 임시 이름을 쓴다. 저장할 때 확정한다.
|
|
1123
1161
|
const draftType = `FIGURE_${Date.now().toString(36).toUpperCase()}`
|
|
1124
1162
|
const { board, parts } = startingModel(draftType)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Explicit AI entry survives lazy creation of the app-level dock. */
|
|
2
|
+
let pending;
|
|
3
|
+
export function requestFigureAI(target) {
|
|
4
|
+
pending = structuredClone(target);
|
|
5
|
+
window.dispatchEvent(new CustomEvent('figure-ai-target'));
|
|
6
|
+
}
|
|
7
|
+
export function takeFigureAITarget() {
|
|
8
|
+
const target = pending;
|
|
9
|
+
pending = undefined;
|
|
10
|
+
return target;
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=figure-ai-target.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"figure-ai-target.js","sourceRoot":"","sources":["../../client/modeller/figure-ai-target.ts"],"names":[],"mappings":"AAAA,sEAAsE;AACtE,IAAI,OAA4D,CAAA;AAChE,MAAM,UAAU,eAAe,CAAC,MAA+C;IAC7E,OAAO,GAAG,eAAe,CAAC,MAAM,CAAC,CAAA;IACjC,MAAM,CAAC,aAAa,CAAC,IAAI,WAAW,CAAC,kBAAkB,CAAC,CAAC,CAAA;AAC3D,CAAC;AACD,MAAM,UAAU,kBAAkB;IAChC,MAAM,MAAM,GAAG,OAAO,CAAA;IACtB,OAAO,GAAG,SAAS,CAAA;IACnB,OAAO,MAAM,CAAA;AACf,CAAC","sourcesContent":["/** Explicit AI entry survives lazy creation of the app-level dock. */\nlet pending: { figureId?: string; source?: unknown } | undefined\nexport function requestFigureAI(target: { figureId?: string; source?: unknown }) {\n pending = structuredClone(target)\n window.dispatchEvent(new CustomEvent('figure-ai-target'))\n}\nexport function takeFigureAITarget() {\n const target = pending\n pending = undefined\n return target\n}\n"]}
|
|
@@ -30,6 +30,7 @@ let FigureAsk = class FigureAsk extends localize(i18next)(LitElement) {
|
|
|
30
30
|
this.type = '';
|
|
31
31
|
/** 이 모델러 세션에서 사람이 명시적으로 남긴 이전 후보 반응. */
|
|
32
32
|
this.feedback = [];
|
|
33
|
+
this.imageOnly = false;
|
|
33
34
|
this.asking = false;
|
|
34
35
|
this.failure = '';
|
|
35
36
|
}
|
|
@@ -61,6 +62,7 @@ let FigureAsk = class FigureAsk extends localize(i18next)(LitElement) {
|
|
|
61
62
|
|
|
62
63
|
form {
|
|
63
64
|
display: flex;
|
|
65
|
+
flex-wrap: wrap;
|
|
64
66
|
flex: 1;
|
|
65
67
|
min-width: 0;
|
|
66
68
|
align-items: center;
|
|
@@ -176,7 +178,7 @@ let FigureAsk = class FigureAsk extends localize(i18next)(LitElement) {
|
|
|
176
178
|
render() {
|
|
177
179
|
const revising = !!this.source;
|
|
178
180
|
return html `
|
|
179
|
-
<form @submit=${(e) => this.submit(e)}>
|
|
181
|
+
<form title="이미지의 형태와 비율을 참고한 후보를 만듭니다. 저장은 하지 않습니다." @submit=${(e) => this.submit(e)}>
|
|
180
182
|
<md-icon lead>auto_awesome</md-icon>
|
|
181
183
|
<span mode>${i18next.t(revising ? 'figure.text.ask-to-revise' : 'figure.text.ask-to-create')}</span>
|
|
182
184
|
<input
|
|
@@ -191,7 +193,7 @@ let FigureAsk = class FigureAsk extends localize(i18next)(LitElement) {
|
|
|
191
193
|
: i18next.t('figure.button.attach-a-picture')}
|
|
192
194
|
<input type="file" accept="image/*" @change=${(e) => this.pick(e)} />
|
|
193
195
|
</label>
|
|
194
|
-
<button go ?disabled=${this.asking}>
|
|
196
|
+
<button go ?disabled=${this.asking || (this.imageOnly && !this.picture)}>
|
|
195
197
|
${this.asking ? i18next.t('figure.text.asking') : i18next.t('figure.button.ask')}
|
|
196
198
|
</button>
|
|
197
199
|
</form>
|
|
@@ -214,15 +216,16 @@ let FigureAsk = class FigureAsk extends localize(i18next)(LitElement) {
|
|
|
214
216
|
e.preventDefault();
|
|
215
217
|
const input = this.renderRoot.querySelector('input[ask]');
|
|
216
218
|
const prompt = input?.value?.trim();
|
|
217
|
-
if (!prompt || this.asking) {
|
|
219
|
+
if (!prompt || this.asking || (this.imageOnly && !this.picture)) {
|
|
218
220
|
return;
|
|
219
221
|
}
|
|
220
222
|
this.asking = true;
|
|
221
223
|
this.failure = '';
|
|
224
|
+
const baseSource = this.source ? JSON.stringify(this.source) : undefined;
|
|
222
225
|
try {
|
|
223
226
|
const proposal = await proposeFigure({
|
|
224
227
|
prompt,
|
|
225
|
-
base:
|
|
228
|
+
base: baseSource,
|
|
226
229
|
type: this.source ? undefined : this.type,
|
|
227
230
|
// 그리는 쪽이 풀 수 있는 토큰만 보낸다. 서버는 이 목록 밖의 색을 거절한다.
|
|
228
231
|
palette: Object.keys(activePalette()),
|
|
@@ -231,7 +234,7 @@ let FigureAsk = class FigureAsk extends localize(i18next)(LitElement) {
|
|
|
231
234
|
feedback: this.feedback,
|
|
232
235
|
image: this.picture
|
|
233
236
|
});
|
|
234
|
-
this.dispatchEvent(new CustomEvent('proposed', { detail: { proposal }, bubbles: true, composed: true }));
|
|
237
|
+
this.dispatchEvent(new CustomEvent('proposed', { detail: { proposal, baseSource }, bubbles: true, composed: true }));
|
|
235
238
|
input.value = '';
|
|
236
239
|
this.picture = undefined;
|
|
237
240
|
}
|
|
@@ -256,6 +259,10 @@ __decorate([
|
|
|
256
259
|
property({ type: Array }),
|
|
257
260
|
__metadata("design:type", Array)
|
|
258
261
|
], FigureAsk.prototype, "feedback", void 0);
|
|
262
|
+
__decorate([
|
|
263
|
+
property({ type: Boolean }),
|
|
264
|
+
__metadata("design:type", Object)
|
|
265
|
+
], FigureAsk.prototype, "imageOnly", void 0);
|
|
259
266
|
__decorate([
|
|
260
267
|
state(),
|
|
261
268
|
__metadata("design:type", Object)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"figure-ask.js","sourceRoot":"","sources":["../../client/modeller/figure-ask.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\n\nimport { css, html, LitElement, nothing } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\n\nimport { i18next, localize } from '@operato/i18n'\nimport type { FigureSource } from '@hatiolab/figure-model'\nimport { activePalette } from '@hatiolab/things-scene'\n\nimport { proposeFigure } from '../graphql/index.js'\nimport type { FigureProposal, ProposalFeedback } from '../types.js'\n\n/**\n * 말로 시키는 줄.\n *\n * ## 무엇을 시키는지가 화면에 보여야 한다\n *\n * 정본이 있으면 **고치는 것**이고, 없으면 **만드는 것**이다. 같은 칸으로 두 가지를\n * 하면 저작자가 「이게 지금 것을 덮어쓰나」를 매번 걱정한다. 그래서 무엇을 할지\n * 칸 옆에 적어 둔다.\n *\n * ## 후보를 여기서 받지 않는다\n *\n * 이 컴포넌트는 시키고 결과를 **알릴 뿐**이다. 받을지 버릴지는 저작면이 정한다 —\n * 정본은 한 벌이고 그 한 벌을 가진 자리가 정해야 한다.\n *\n * ## 팔레트를 함께 보낸다\n *\n * 서버는 팔레트를 모른다. 그리는 쪽이 어떤 토큰을 풀 수 있는지 아는 유일한 자리다.\n */\n@customElement('figure-ask')\nexport class FigureAsk extends localize(i18next)(LitElement) {\n static styles = css`\n /*\n 이 부품은 **자기 줄을 갖지 않는다.**\n\n 전에는 테두리와 여백으로 스스로 한 줄이 되었다. 저작면의 머리줄 바로 아래에 놓이니\n 화면 위쪽이 두 줄이 되었고, 위 줄은 이름을 페이지 제목과 겹쳐 적고 있었다. 캔버스가\n 주인공인 화면에서 그 두 줄은 비싸다.\n\n 이제 머리줄 안에 들어가 그 줄의 남는 폭을 쓴다. 테두리·여백은 담는 줄이 갖는다.\n */\n :host {\n display: flex;\n flex: 1;\n min-width: 0;\n font: var(--label-font, inherit);\n /*\n 이 줄의 글자 크기를 여기서 정한다.\n\n 안 적어 두면 브라우저 기본 크기(16px)를 물려받는다. 아래 패널들은 전부 12px\n 언저리라 위 두 줄만 커 보이고, 단추가 화면 전체와 따로 논다.\n\n form 요소(input · button)는 글꼴을 물려받지 않으므로 각자 적어야 한다.\n */\n font-size: 0.76rem;\n }\n\n form {\n display: flex;\n flex: 1;\n min-width: 0;\n align-items: center;\n gap: var(--spacing-medium, 8px);\n }\n\n md-icon[lead] {\n --md-icon-size: 18px;\n flex: none;\n color: var(--md-sys-color-primary);\n }\n\n input[ask] {\n flex: 1;\n min-width: 0;\n padding: 6px 10px;\n font: var(--input-field-font, inherit);\n font-size: 0.78rem;\n color: var(--md-sys-color-on-surface);\n background-color: var(--md-sys-color-surface-container-lowest);\n border: 1px solid var(--md-sys-color-outline-variant);\n border-radius: 6px;\n }\n input[ask]:focus {\n outline: 2px solid var(--md-sys-color-primary);\n outline-offset: -1px;\n }\n input[ask][disabled] {\n opacity: 0.5;\n }\n\n /* 무엇을 시키는 것인지 — 만드나 고치나 */\n span[mode] {\n flex: none;\n font-size: 0.72rem;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n }\n\n /* 그림 붙이기. 붙인 것이 있으면 이름을 보여 준다 */\n label[picture] {\n display: flex;\n align-items: center;\n gap: 4px;\n flex: none;\n padding: 5px 9px;\n font-size: 0.74rem;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n border: 1px solid var(--md-sys-color-outline-variant);\n border-radius: 6px;\n cursor: pointer;\n }\n label[picture]:hover {\n background-color: var(--md-sys-color-surface-container);\n }\n label[picture][has] {\n color: var(--md-sys-color-on-primary-container);\n background-color: var(--md-sys-color-primary-container);\n border-color: transparent;\n }\n label[picture] md-icon {\n --md-icon-size: 16px;\n }\n label[picture] input {\n display: none;\n }\n span[filename] {\n max-width: 8em;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n\n /*\n 채운 파랑 단추를 저장과 나눠 갖지 않는다.\n\n 머리줄의 저장이 이 화면의 주 동작이다. 바로 아래 줄에 같은 파랑 단추가 하나 더\n 있으면 어느 쪽이 주인지 눈이 못 고른다. 이쪽은 톤 단추로 한 단계 내린다.\n */\n button[go] {\n flex: none;\n padding: 6px 14px;\n font: var(--label-font, inherit);\n font-size: 0.76rem;\n font-weight: 600;\n color: var(--md-sys-color-on-primary-container);\n background-color: var(--md-sys-color-primary-container);\n border: none;\n border-radius: 6px;\n cursor: pointer;\n }\n button[go]:hover:not([disabled]) {\n background-color: color-mix(in srgb, var(--md-sys-color-primary) 24%, var(--md-sys-color-primary-container));\n }\n button[go][disabled] {\n opacity: 0.4;\n cursor: default;\n }\n\n /* 못 만들었을 때. 사유를 삼키지 않는다 — 말을 고쳐 다시 시킬 단서다 */\n div[failed] {\n display: flex;\n gap: 6px;\n padding: 0 var(--spacing-large, 12px) var(--spacing-medium, 8px) var(--spacing-large, 12px);\n font-size: 0.74rem;\n line-height: 1.5;\n color: var(--md-sys-color-error);\n }\n div[failed] md-icon {\n --md-icon-size: 16px;\n flex: none;\n }\n `\n\n /** 지금 정본. 있으면 고치는 것이고 없으면 만드는 것이다. */\n @property({ type: Object }) source?: FigureSource\n /** 새로 만들 때 쓸 타입 이름. */\n @property({ type: String }) type = ''\n /** 이 모델러 세션에서 사람이 명시적으로 남긴 이전 후보 반응. */\n @property({ type: Array }) feedback: ProposalFeedback[] = []\n\n @state() private asking = false\n @state() private failure = ''\n @state() private picture?: File\n\n render() {\n const revising = !!this.source\n\n return html`\n <form @submit=${(e: Event) => this.submit(e)}>\n <md-icon lead>auto_awesome</md-icon>\n <span mode>${i18next.t(revising ? 'figure.text.ask-to-revise' : 'figure.text.ask-to-create')}</span>\n <input\n ask\n ?disabled=${this.asking}\n placeholder=${i18next.t(revising ? 'figure.text.ask-revise-example' : 'figure.text.ask-create-example')}\n />\n <label picture ?has=${!!this.picture} title=${i18next.t('figure.text.picture-is-for-proportions')}>\n <md-icon>image</md-icon>\n ${this.picture\n ? html`<span filename>${this.picture.name}</span>`\n : i18next.t('figure.button.attach-a-picture')}\n <input type=\"file\" accept=\"image/*\" @change=${(e: Event) => this.pick(e)} />\n </label>\n <button go ?disabled=${this.asking}>\n ${this.asking ? i18next.t('figure.text.asking') : i18next.t('figure.button.ask')}\n </button>\n </form>\n\n ${this.failure\n ? html`\n <div failed>\n <md-icon>error</md-icon>\n <span>${this.failure}</span>\n </div>\n `\n : nothing}\n `\n }\n\n private pick(e: Event) {\n const input = e.target as HTMLInputElement\n this.picture = input.files?.[0]\n }\n\n private async submit(e: Event) {\n e.preventDefault()\n\n const input = this.renderRoot.querySelector('input[ask]') as HTMLInputElement\n const prompt = input?.value?.trim()\n if (!prompt || this.asking) {\n return\n }\n\n this.asking = true\n this.failure = ''\n\n try {\n const proposal = await proposeFigure({\n prompt,\n base: this.source ? JSON.stringify(this.source) : undefined,\n type: this.source ? undefined : this.type,\n // 그리는 쪽이 풀 수 있는 토큰만 보낸다. 서버는 이 목록 밖의 색을 거절한다.\n palette: Object.keys(activePalette() as Record<string, string>),\n // 구조적으로는 맞지만 품질 근거가 남으면 한 번 더 개선 후보를 시킨다.\n refine: true,\n feedback: this.feedback,\n image: this.picture\n })\n\n this.dispatchEvent(\n new CustomEvent('proposed', { detail: { proposal }, bubbles: true, composed: true })\n )\n input.value = ''\n this.picture = undefined\n } catch (err) {\n // 사유를 그대로 보여 준다. 「실패했습니다」로 뭉개면 말을 어떻게 고칠지 모른다.\n this.failure = (err as Error).message\n } finally {\n this.asking = false\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"figure-ask.js","sourceRoot":"","sources":["../../client/modeller/figure-ask.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\n\nimport { css, html, LitElement, nothing } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\n\nimport { i18next, localize } from '@operato/i18n'\nimport type { FigureSource } from '@hatiolab/figure-model'\nimport { activePalette } from '@hatiolab/things-scene'\n\nimport { proposeFigure } from '../graphql/index.js'\nimport type { FigureProposal, ProposalFeedback } from '../types.js'\n\n/**\n * 말로 시키는 줄.\n *\n * ## 무엇을 시키는지가 화면에 보여야 한다\n *\n * 정본이 있으면 **고치는 것**이고, 없으면 **만드는 것**이다. 같은 칸으로 두 가지를\n * 하면 저작자가 「이게 지금 것을 덮어쓰나」를 매번 걱정한다. 그래서 무엇을 할지\n * 칸 옆에 적어 둔다.\n *\n * ## 후보를 여기서 받지 않는다\n *\n * 이 컴포넌트는 시키고 결과를 **알릴 뿐**이다. 받을지 버릴지는 저작면이 정한다 —\n * 정본은 한 벌이고 그 한 벌을 가진 자리가 정해야 한다.\n *\n * ## 팔레트를 함께 보낸다\n *\n * 서버는 팔레트를 모른다. 그리는 쪽이 어떤 토큰을 풀 수 있는지 아는 유일한 자리다.\n */\n@customElement('figure-ask')\nexport class FigureAsk extends localize(i18next)(LitElement) {\n static styles = css`\n /*\n 이 부품은 **자기 줄을 갖지 않는다.**\n\n 전에는 테두리와 여백으로 스스로 한 줄이 되었다. 저작면의 머리줄 바로 아래에 놓이니\n 화면 위쪽이 두 줄이 되었고, 위 줄은 이름을 페이지 제목과 겹쳐 적고 있었다. 캔버스가\n 주인공인 화면에서 그 두 줄은 비싸다.\n\n 이제 머리줄 안에 들어가 그 줄의 남는 폭을 쓴다. 테두리·여백은 담는 줄이 갖는다.\n */\n :host {\n display: flex;\n flex: 1;\n min-width: 0;\n font: var(--label-font, inherit);\n /*\n 이 줄의 글자 크기를 여기서 정한다.\n\n 안 적어 두면 브라우저 기본 크기(16px)를 물려받는다. 아래 패널들은 전부 12px\n 언저리라 위 두 줄만 커 보이고, 단추가 화면 전체와 따로 논다.\n\n form 요소(input · button)는 글꼴을 물려받지 않으므로 각자 적어야 한다.\n */\n font-size: 0.76rem;\n }\n\n form {\n display: flex;\n flex-wrap: wrap;\n flex: 1;\n min-width: 0;\n align-items: center;\n gap: var(--spacing-medium, 8px);\n }\n\n md-icon[lead] {\n --md-icon-size: 18px;\n flex: none;\n color: var(--md-sys-color-primary);\n }\n\n input[ask] {\n flex: 1;\n min-width: 0;\n padding: 6px 10px;\n font: var(--input-field-font, inherit);\n font-size: 0.78rem;\n color: var(--md-sys-color-on-surface);\n background-color: var(--md-sys-color-surface-container-lowest);\n border: 1px solid var(--md-sys-color-outline-variant);\n border-radius: 6px;\n }\n input[ask]:focus {\n outline: 2px solid var(--md-sys-color-primary);\n outline-offset: -1px;\n }\n input[ask][disabled] {\n opacity: 0.5;\n }\n\n /* 무엇을 시키는 것인지 — 만드나 고치나 */\n span[mode] {\n flex: none;\n font-size: 0.72rem;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n }\n\n /* 그림 붙이기. 붙인 것이 있으면 이름을 보여 준다 */\n label[picture] {\n display: flex;\n align-items: center;\n gap: 4px;\n flex: none;\n padding: 5px 9px;\n font-size: 0.74rem;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n border: 1px solid var(--md-sys-color-outline-variant);\n border-radius: 6px;\n cursor: pointer;\n }\n label[picture]:hover {\n background-color: var(--md-sys-color-surface-container);\n }\n label[picture][has] {\n color: var(--md-sys-color-on-primary-container);\n background-color: var(--md-sys-color-primary-container);\n border-color: transparent;\n }\n label[picture] md-icon {\n --md-icon-size: 16px;\n }\n label[picture] input {\n display: none;\n }\n span[filename] {\n max-width: 8em;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n\n /*\n 채운 파랑 단추를 저장과 나눠 갖지 않는다.\n\n 머리줄의 저장이 이 화면의 주 동작이다. 바로 아래 줄에 같은 파랑 단추가 하나 더\n 있으면 어느 쪽이 주인지 눈이 못 고른다. 이쪽은 톤 단추로 한 단계 내린다.\n */\n button[go] {\n flex: none;\n padding: 6px 14px;\n font: var(--label-font, inherit);\n font-size: 0.76rem;\n font-weight: 600;\n color: var(--md-sys-color-on-primary-container);\n background-color: var(--md-sys-color-primary-container);\n border: none;\n border-radius: 6px;\n cursor: pointer;\n }\n button[go]:hover:not([disabled]) {\n background-color: color-mix(in srgb, var(--md-sys-color-primary) 24%, var(--md-sys-color-primary-container));\n }\n button[go][disabled] {\n opacity: 0.4;\n cursor: default;\n }\n\n /* 못 만들었을 때. 사유를 삼키지 않는다 — 말을 고쳐 다시 시킬 단서다 */\n div[failed] {\n display: flex;\n gap: 6px;\n padding: 0 var(--spacing-large, 12px) var(--spacing-medium, 8px) var(--spacing-large, 12px);\n font-size: 0.74rem;\n line-height: 1.5;\n color: var(--md-sys-color-error);\n }\n div[failed] md-icon {\n --md-icon-size: 16px;\n flex: none;\n }\n `\n\n /** 지금 정본. 있으면 고치는 것이고 없으면 만드는 것이다. */\n @property({ type: Object }) source?: FigureSource\n /** 새로 만들 때 쓸 타입 이름. */\n @property({ type: String }) type = ''\n /** 이 모델러 세션에서 사람이 명시적으로 남긴 이전 후보 반응. */\n @property({ type: Array }) feedback: ProposalFeedback[] = []\n @property({ type: Boolean }) imageOnly = false\n\n @state() private asking = false\n @state() private failure = ''\n @state() private picture?: File\n\n render() {\n const revising = !!this.source\n\n return html`\n <form title=\"이미지의 형태와 비율을 참고한 후보를 만듭니다. 저장은 하지 않습니다.\" @submit=${(e: Event) => this.submit(e)}>\n <md-icon lead>auto_awesome</md-icon>\n <span mode>${i18next.t(revising ? 'figure.text.ask-to-revise' : 'figure.text.ask-to-create')}</span>\n <input\n ask\n ?disabled=${this.asking}\n placeholder=${i18next.t(revising ? 'figure.text.ask-revise-example' : 'figure.text.ask-create-example')}\n />\n <label picture ?has=${!!this.picture} title=${i18next.t('figure.text.picture-is-for-proportions')}>\n <md-icon>image</md-icon>\n ${this.picture\n ? html`<span filename>${this.picture.name}</span>`\n : i18next.t('figure.button.attach-a-picture')}\n <input type=\"file\" accept=\"image/*\" @change=${(e: Event) => this.pick(e)} />\n </label>\n <button go ?disabled=${this.asking || (this.imageOnly && !this.picture)}>\n ${this.asking ? i18next.t('figure.text.asking') : i18next.t('figure.button.ask')}\n </button>\n </form>\n\n ${this.failure\n ? html`\n <div failed>\n <md-icon>error</md-icon>\n <span>${this.failure}</span>\n </div>\n `\n : nothing}\n `\n }\n\n private pick(e: Event) {\n const input = e.target as HTMLInputElement\n this.picture = input.files?.[0]\n }\n\n private async submit(e: Event) {\n e.preventDefault()\n\n const input = this.renderRoot.querySelector('input[ask]') as HTMLInputElement\n const prompt = input?.value?.trim()\n if (!prompt || this.asking || (this.imageOnly && !this.picture)) {\n return\n }\n\n this.asking = true\n this.failure = ''\n const baseSource = this.source ? JSON.stringify(this.source) : undefined\n\n try {\n const proposal = await proposeFigure({\n prompt,\n base: baseSource,\n type: this.source ? undefined : this.type,\n // 그리는 쪽이 풀 수 있는 토큰만 보낸다. 서버는 이 목록 밖의 색을 거절한다.\n palette: Object.keys(activePalette() as Record<string, string>),\n // 구조적으로는 맞지만 품질 근거가 남으면 한 번 더 개선 후보를 시킨다.\n refine: true,\n feedback: this.feedback,\n image: this.picture\n })\n\n this.dispatchEvent(\n new CustomEvent('proposed', { detail: { proposal, baseSource }, bubbles: true, composed: true })\n )\n input.value = ''\n this.picture = undefined\n } catch (err) {\n // 사유를 그대로 보여 준다. 「실패했습니다」로 뭉개면 말을 어떻게 고칠지 모른다.\n this.failure = (err as Error).message\n } finally {\n this.asking = false\n }\n }\n}\n"]}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** A route entry starts a new document; ordinary updates must preserve edits. */
|
|
2
|
+
export function shouldStartNewFigure(changes, hasFigure, savedId) {
|
|
3
|
+
return !hasFigure || !!savedId || changes.active === true || Object.prototype.hasOwnProperty.call(changes, 'resourceId');
|
|
4
|
+
}
|
|
5
|
+
//# sourceMappingURL=new-figure-entry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"new-figure-entry.js","sourceRoot":"","sources":["../../client/modeller/new-figure-entry.ts"],"names":[],"mappings":"AAAA,iFAAiF;AACjF,MAAM,UAAU,oBAAoB,CAAC,OAAgC,EAAE,SAAkB,EAAE,OAAgB;IACzG,OAAO,CAAC,SAAS,IAAI,CAAC,CAAC,OAAO,IAAI,OAAO,CAAC,MAAM,KAAK,IAAI,IAAI,MAAM,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,OAAO,EAAE,YAAY,CAAC,CAAA;AAC1H,CAAC","sourcesContent":["/** A route entry starts a new document; ordinary updates must preserve edits. */\nexport function shouldStartNewFigure(changes: Record<string, unknown>, hasFigure: boolean, savedId?: string): boolean {\n return !hasFigure || !!savedId || changes.active === true || Object.prototype.hasOwnProperty.call(changes, 'resourceId')\n}\n"]}
|
|
@@ -88,6 +88,10 @@ export declare class FigureModellerPage extends FigureModellerPageBase {
|
|
|
88
88
|
* 초안이 다음 Figure에 섞이지 않게 한다.
|
|
89
89
|
*/
|
|
90
90
|
updated(changed: Map<string, unknown>): void;
|
|
91
|
+
private announceAiContext;
|
|
92
|
+
private acceptDockProposal;
|
|
93
|
+
private proposalBaseSource?;
|
|
94
|
+
connectedCallback(): void;
|
|
91
95
|
disconnectedCallback(): void;
|
|
92
96
|
get context(): {
|
|
93
97
|
title: string;
|
|
@@ -202,6 +206,7 @@ export declare class FigureModellerPage extends FigureModellerPageBase {
|
|
|
202
206
|
pageUpdated(changes: Record<string, unknown>, lifecycle: {
|
|
203
207
|
resourceId?: string;
|
|
204
208
|
}): Promise<void>;
|
|
209
|
+
private loadGeneration;
|
|
205
210
|
private load;
|
|
206
211
|
private startNew;
|
|
207
212
|
private rename;
|