@things-factory/ai-assistant 10.1.28 → 10.1.29
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/components/assistant-chat.ts +51 -7
- package/client/components/assistant-mode.test.ts +20 -1
- package/client/components/assistant-mode.ts +20 -0
- package/client/components/panel-aria.test.ts +20 -0
- package/client/utils/assistant-session-controller.ts +14 -1
- package/client/utils/assistant-session-transport.ts +2 -2
- package/dist-client/components/assistant-chat.d.ts +12 -0
- package/dist-client/components/assistant-chat.js +48 -6
- package/dist-client/components/assistant-chat.js.map +1 -1
- package/dist-client/components/assistant-mode.d.ts +22 -0
- package/dist-client/components/assistant-mode.js +16 -0
- package/dist-client/components/assistant-mode.js.map +1 -1
- package/dist-client/components/assistant-mode.test.js +17 -1
- package/dist-client/components/assistant-mode.test.js.map +1 -1
- package/dist-client/components/panel-aria.test.js +18 -0
- package/dist-client/components/panel-aria.test.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/utils/assistant-session-controller.d.ts +10 -0
- package/dist-client/utils/assistant-session-controller.js.map +1 -1
- package/dist-client/utils/assistant-session-transport.js +2 -2
- package/dist-client/utils/assistant-session-transport.js.map +1 -1
- package/dist-server/door-declaration-report.d.ts +14 -0
- package/dist-server/door-declaration-report.js +54 -0
- package/dist-server/door-declaration-report.js.map +1 -0
- package/dist-server/service/assistant-chat-resolver.js +19 -0
- package/dist-server/service/assistant-chat-resolver.js.map +1 -1
- package/dist-server/service/assistant-mode/assistant-mode-resolver.d.ts +8 -0
- package/dist-server/service/assistant-mode/assistant-mode-resolver.js +8 -0
- package/dist-server/service/assistant-mode/assistant-mode-resolver.js.map +1 -1
- package/dist-server/service/assistant-mode/resolve.d.ts +10 -0
- package/dist-server/service/assistant-mode/resolve.js +5 -0
- package/dist-server/service/assistant-mode/resolve.js.map +1 -1
- package/dist-server/service/assistant-mode/types.d.ts +21 -0
- package/dist-server/service/assistant-mode/types.js.map +1 -1
- package/dist-server/service/chat-message/carried-turns.js +14 -2
- package/dist-server/service/chat-message/carried-turns.js.map +1 -1
- package/dist-server/service/chat-message/door-access.d.ts +19 -7
- package/dist-server/service/chat-message/door-access.js +19 -11
- package/dist-server/service/chat-message/door-access.js.map +1 -1
- package/dist-server/service/chat-message/reader-gate.js +6 -4
- package/dist-server/service/chat-message/reader-gate.js.map +1 -1
- package/dist-server/service/chat-session/chat-session.d.ts +12 -0
- package/dist-server/service/chat-session/chat-session.js +12 -11
- package/dist-server/service/chat-session/chat-session.js.map +1 -1
- package/dist-server/service/chat-session/session-anchor.d.ts +10 -1
- package/dist-server/service/chat-session/session-anchor.js +12 -4
- package/dist-server/service/chat-session/session-anchor.js.map +1 -1
- package/dist-server/service/chat-session/session-inbox.d.ts +2 -0
- package/dist-server/service/chat-session/session-inbox.js.map +1 -1
- package/dist-server/service/chat-session/session-kind.d.ts +14 -0
- package/dist-server/service/chat-session/session-kind.js +30 -0
- package/dist-server/service/chat-session/session-kind.js.map +1 -0
- package/dist-server/service/chat-session/session-visibility.js +8 -1
- package/dist-server/service/chat-session/session-visibility.js.map +1 -1
- package/dist-server/service/chat-session-resolver.d.ts +2 -0
- package/dist-server/service/chat-session-resolver.js +4 -0
- package/dist-server/service/chat-session-resolver.js.map +1 -1
- package/dist-server/service/index.d.ts +1 -0
- package/dist-server/service/index.js +2 -0
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/service/testing/mode-conformance.d.ts +10 -0
- package/dist-server/service/testing/mode-conformance.js +13 -0
- package/dist-server/service/testing/mode-conformance.js.map +1 -1
- package/dist-server/tool-registry-boot-report.js +7 -0
- package/dist-server/tool-registry-boot-report.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/server/door-declaration-report.test.ts +53 -0
- package/server/door-declaration-report.ts +61 -0
- package/server/service/assistant-chat-resolver.ts +21 -0
- package/server/service/assistant-mode/assistant-mode-resolver.ts +6 -0
- package/server/service/assistant-mode/resolve.test.ts +42 -0
- package/server/service/assistant-mode/resolve.ts +9 -0
- package/server/service/assistant-mode/types.ts +18 -0
- package/server/service/chat-message/carried-turns.test.ts +13 -2
- package/server/service/chat-message/carried-turns.ts +16 -3
- package/server/service/chat-message/door-access.test.ts +72 -0
- package/server/service/chat-message/door-access.ts +43 -23
- package/server/service/chat-message/fold-history-window.test.ts +184 -0
- package/server/service/chat-message/reader-gate.ts +6 -4
- package/server/service/chat-session/chat-session.ts +15 -0
- package/server/service/chat-session/session-anchor.ts +12 -3
- package/server/service/chat-session/session-inbox.ts +2 -0
- package/server/service/chat-session/session-kind.test.ts +24 -0
- package/server/service/chat-session/session-kind.ts +32 -0
- package/server/service/chat-session/session-visibility.test.ts +16 -0
- package/server/service/chat-session/session-visibility.ts +9 -1
- package/server/service/chat-session-resolver.ts +4 -0
- package/server/service/index.ts +2 -0
- package/server/service/testing/mode-conformance.ts +15 -0
- package/server/tool-registry-boot-report.ts +6 -0
- package/translations/en.json +3 -2
- package/translations/ja.json +3 -2
- package/translations/ko.json +3 -2
- package/translations/ms.json +3 -2
- package/translations/zh.json +3 -2
|
@@ -219,6 +219,8 @@ const ASSISTANT_MODE_QUERY = gql`
|
|
|
219
219
|
catalogEntries
|
|
220
220
|
proposalChoices
|
|
221
221
|
placeholder
|
|
222
|
+
intro
|
|
223
|
+
examples
|
|
222
224
|
footerNotice
|
|
223
225
|
stagedNotice
|
|
224
226
|
}
|
|
@@ -563,6 +565,22 @@ export class OxAssistantChat extends LitElement {
|
|
|
563
565
|
@state()
|
|
564
566
|
private dragOver = false
|
|
565
567
|
|
|
568
|
+
/*
|
|
569
|
+
* ── 왜 이미지만 받나 ──────────────────────────────────────────────────────
|
|
570
|
+
* `.json` · `.txt` · `.csv` 도 받고 있었다. 그 파일들은 `hostContext.attachments` 로 실려 갔고
|
|
571
|
+
* **그것을 읽어 모델에 붙이는 코드가 없었다.** 사람이 CSV 를 붙이고 「이 표 좀 봐줘」라고 하면 화면에
|
|
572
|
+
* 파일 칩이 뜨고 답도 왔다. 그 답은 파일을 본 적이 없고, 오류도 안 났다(2026-09-18 표면 조사).
|
|
573
|
+
*
|
|
574
|
+
* 이미지는 길이 있다 — `assistant-chat-resolver.ts` 가 `hostContext.image` 를 마지막 사람 턴에
|
|
575
|
+
* 붙인다. 그래서 되는 것만 남기고 좁혔다. 못 하는 일을 화면이 권하지 않는다.
|
|
576
|
+
*
|
|
577
|
+
* ── 글 파일을 청하는 제품이 설 때의 모양 (수석 아키텍트, ADR-0071 보탬) ───
|
|
578
|
+
* 이미지와 **같은 자리**(사람 턴)에 붙인다. 크기 상한은 모드가 선언하고, 넘으면 보내기 전에 사실 한
|
|
579
|
+
* 줄로 거절한다 — 조용히 자르지 않는다. 비용은 사용량의 축이다(ADR-0061 결정 6).
|
|
580
|
+
*
|
|
581
|
+
* `attachments` 짐 자체는 그대로 둔다: figure 의 도구가 거기서 이미지를 찾는다
|
|
582
|
+
* (`figure-tools.ts` — `ctx.host.image` 가 없을 때의 갈래).
|
|
583
|
+
*/
|
|
566
584
|
private async onAttachFiles(files: FileList | File[] | null | undefined) {
|
|
567
585
|
if (!files || files.length === 0) return
|
|
568
586
|
const list = Array.from(files)
|
|
@@ -875,13 +893,29 @@ export class OxAssistantChat extends LitElement {
|
|
|
875
893
|
*/
|
|
876
894
|
private onModePick(kind: string) {
|
|
877
895
|
if (!this.mode || kind === this.mode.kind) return
|
|
878
|
-
this.mode = { ...this.mode, kind }
|
|
879
896
|
try {
|
|
880
897
|
if (this.subject) localStorage.setItem(`assistant-mode:${this.subject}`, kind)
|
|
881
898
|
} catch {
|
|
882
899
|
/* 저장이 막힌 브라우저에서도 고르기 자체는 된다 — 다음에 다시 고르면 될 뿐이다. */
|
|
883
900
|
}
|
|
884
|
-
this.
|
|
901
|
+
this.askHostForMode(kind)
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
/**
|
|
905
|
+
* **고르는 것은 청하는 일이다** — `mode` 를 여기서 바꾸지 않는다.
|
|
906
|
+
*
|
|
907
|
+
* host 는 렌더마다 `mode` 를 다시 내려 준다. 패널이 자기 손으로 고쳐 두면 다음 렌더가 그것을 덮고,
|
|
908
|
+
* 더 나쁜 것은 host 가 **자기가 무엇을 열었는지 모르는 채로 남는다**는 것이다. host 는 대화의 세션을
|
|
909
|
+
* 대상별로 들고 있으므로, 대화가 바뀐 것을 모르면 앞 대화의 세션을 그대로 넘긴다 — 그러면 새 말이 앞
|
|
910
|
+
* 대화에 붙는다(2026-09-18 Figure 레인이 :3300 에서 본 것이 이것이다).
|
|
911
|
+
*
|
|
912
|
+
* 그래서 주인은 하나다: host 가 `mode` 를 갖고, 패널은 「이것으로 바꿔 달라」고 말한다. 기억은 패널이
|
|
913
|
+
* 진다(브라우저마다 다른 값이라 host 가 질 것이 아니다) — 기억한 것을 되살릴 때도 같은 청으로 나간다.
|
|
914
|
+
*/
|
|
915
|
+
private askHostForMode(kind: string) {
|
|
916
|
+
this.dispatchEvent(
|
|
917
|
+
new CustomEvent('assistant-mode-change', { detail: { kind, subject: this.subject }, bubbles: true, composed: true })
|
|
918
|
+
)
|
|
885
919
|
}
|
|
886
920
|
|
|
887
921
|
/** 이 subject 에서 지난번에 고른 대화 — 없으면 undefined. */
|
|
@@ -2541,7 +2575,7 @@ export class OxAssistantChat extends LitElement {
|
|
|
2541
2575
|
if (this.modeChoices.length < 2 || !this.subject || !this.mode) return
|
|
2542
2576
|
|
|
2543
2577
|
const kind = pickInitialMode(this.mode.kind, this.modeChoices, this.rememberedMode(this.subject))
|
|
2544
|
-
if (kind && kind !== this.mode.kind) this.
|
|
2578
|
+
if (kind && kind !== this.mode.kind) this.askHostForMode(kind)
|
|
2545
2579
|
}
|
|
2546
2580
|
|
|
2547
2581
|
updated(changed: Map<string, any>) {
|
|
@@ -2905,7 +2939,7 @@ export class OxAssistantChat extends LitElement {
|
|
|
2905
2939
|
id="chat-file-input"
|
|
2906
2940
|
type="file"
|
|
2907
2941
|
multiple
|
|
2908
|
-
accept="image
|
|
2942
|
+
accept="image/*"
|
|
2909
2943
|
style="display: none"
|
|
2910
2944
|
@change=${(e: Event) => {
|
|
2911
2945
|
const input = e.target as HTMLInputElement
|
|
@@ -2919,7 +2953,7 @@ export class OxAssistantChat extends LitElement {
|
|
|
2919
2953
|
type="button"
|
|
2920
2954
|
class="action"
|
|
2921
2955
|
@click=${() => this.renderRoot.querySelector<HTMLInputElement>('#chat-file-input')?.click()}
|
|
2922
|
-
title=${i18next.t('ai-assistant.tooltip.attach-file', { defaultValue: '
|
|
2956
|
+
title=${i18next.t('ai-assistant.tooltip.attach-file', { defaultValue: 'attach an image (drag & drop or paste supported)' })}>
|
|
2923
2957
|
<md-icon>attach_file</md-icon>
|
|
2924
2958
|
${i18next.t('ai-assistant.button.attach', { defaultValue: 'Attach' })}
|
|
2925
2959
|
</button>
|
|
@@ -3444,6 +3478,11 @@ export class OxAssistantChat extends LitElement {
|
|
|
3444
3478
|
* 읽었고, 그러면 호스트 값을 한쪽만 반영하는 상태가 만들어질 수 있었다.
|
|
3445
3479
|
*/
|
|
3446
3480
|
private get exampleGroups(): Array<{ label: string; items: string[] }> {
|
|
3481
|
+
/*
|
|
3482
|
+
* 대화가 자기 예시를 가지면 그것이 먼저다 — 자리표·`/` 와 같은 차례(ADR-0061 결정 7). host 속성은
|
|
3483
|
+
* 대화를 모르므로, 한 화면에 대화가 둘 서면 한쪽에 맞는 글이 다른 쪽에도 뜬다.
|
|
3484
|
+
*/
|
|
3485
|
+
if (this.modeView?.examples?.length) return this.modeView.examples
|
|
3447
3486
|
if (this.examples?.length) return this.examples
|
|
3448
3487
|
return OxAssistantChat.EXAMPLE_GROUPS.map(group => ({
|
|
3449
3488
|
label: i18next.t(group.labelKey),
|
|
@@ -3605,8 +3644,13 @@ export class OxAssistantChat extends LitElement {
|
|
|
3605
3644
|
<div class="empty">
|
|
3606
3645
|
<div class="header">
|
|
3607
3646
|
<md-icon class="icon">auto_awesome</md-icon>
|
|
3608
|
-
|
|
3609
|
-
<div class="
|
|
3647
|
+
<!-- 대화가 자기 글을 가지면 그것이 먼저다 — host 속성은 대화를 모른다. -->
|
|
3648
|
+
<div class="title">
|
|
3649
|
+
${this.modeView?.intro?.title ?? this.intro?.title ?? i18next.t('ai-assistant.text.empty-title')}
|
|
3650
|
+
</div>
|
|
3651
|
+
<div class="subtitle">
|
|
3652
|
+
${this.modeView?.intro?.subtitle ?? this.intro?.subtitle ?? i18next.t('ai-assistant.text.empty-subtitle')}
|
|
3653
|
+
</div>
|
|
3610
3654
|
</div>
|
|
3611
3655
|
|
|
3612
3656
|
${this.triggerChips.length > 0
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* the slash list was a module global. A mode's wording now comes from the same declaration the
|
|
10
10
|
* door reads, so the two cannot describe different conversations.
|
|
11
11
|
*/
|
|
12
|
-
import { isDifferentConversation, resolveConversation, type AssistantMode, pickInitialMode } from './assistant-mode'
|
|
12
|
+
import { isDifferentConversation, resolveConversation, type AssistantMode, pickInitialMode, followPickedConversation } from './assistant-mode'
|
|
13
13
|
|
|
14
14
|
const MODE: AssistantMode = { kind: 'twin.space', target: 'S-12', hostContext: { spaceId: 'S-12' } }
|
|
15
15
|
|
|
@@ -126,3 +126,22 @@ describe('pickInitialMode', () => {
|
|
|
126
126
|
expect(pickInitialMode('twin.board', choices, undefined)).toBe('twin.board')
|
|
127
127
|
})
|
|
128
128
|
})
|
|
129
|
+
|
|
130
|
+
/*
|
|
131
|
+
* ADR-0072 결정 3 — 목록에서 고른 대화로 화면이 따라간다.
|
|
132
|
+
*/
|
|
133
|
+
describe('followPickedConversation', () => {
|
|
134
|
+
it('★ 다른 대화의 세션을 고르면 그 대화로 간다 — 안 따라가면 문이 거절한다', () => {
|
|
135
|
+
expect(followPickedConversation('figure.catalog', 'figure.modeller')).toBe('figure.catalog')
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('같은 대화면 그대로 둔다', () => {
|
|
139
|
+
expect(followPickedConversation('figure.modeller', 'figure.modeller')).toBeUndefined()
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('★ 칸이 서기 전 세션은 지금 대화로 둔다 — 서버가 다음 턴에 찍는다', () => {
|
|
143
|
+
expect(followPickedConversation(undefined, 'figure.modeller')).toBeUndefined()
|
|
144
|
+
expect(followPickedConversation(null, 'figure.modeller')).toBeUndefined()
|
|
145
|
+
expect(followPickedConversation('', 'figure.modeller')).toBeUndefined()
|
|
146
|
+
})
|
|
147
|
+
})
|
|
@@ -47,6 +47,10 @@ export interface AssistantModeView {
|
|
|
47
47
|
catalogEntries: any[]
|
|
48
48
|
proposalChoices: any[]
|
|
49
49
|
placeholder?: string
|
|
50
|
+
/** 빈 대화의 제목과 한 줄 — 이 대화가 무엇을 하는 자리인지. host 속성보다 앞선다. */
|
|
51
|
+
intro?: { title: string; subtitle: string }
|
|
52
|
+
/** 빈 대화의 예시 — 이 대화에서 실제로 할 수 있는 말. host 속성보다 앞선다. */
|
|
53
|
+
examples?: Array<{ label: string; items: string[] }>
|
|
50
54
|
footerNotice?: string
|
|
51
55
|
stagedNotice?: string
|
|
52
56
|
}
|
|
@@ -170,3 +174,19 @@ export function pickInitialMode(
|
|
|
170
174
|
if (!remembered || remembered === current) return current
|
|
171
175
|
return choices.some(choice => choice.kind === remembered) ? remembered : current
|
|
172
176
|
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* 목록에서 고른 대화로 따라갈 것인가 — ADR-0072 결정 3.
|
|
180
|
+
*
|
|
181
|
+
* 목록은 한 대상의 대화를 **모두** 보여 주고, 그 대상 위에는 대화가 둘 설 수 있다(A17). 고른 것이 지금
|
|
182
|
+
* 열린 대화와 다른데 화면이 그대로 있으면 문이 거절하고(`session-kind-mismatch`), 목록은 눌리는데 안
|
|
183
|
+
* 열리는 목록이 된다 — 선언은 있고 길이 없는 그 모양이다.
|
|
184
|
+
*
|
|
185
|
+
* 칸이 서기 전에 난 세션에는 값이 없다. 그때는 지금 대화로 둔다 — 서버가 다음 턴에 지금 대화로 찍는다.
|
|
186
|
+
*
|
|
187
|
+
* `undefined` 는 「그대로 둔다」는 뜻이다.
|
|
188
|
+
*/
|
|
189
|
+
export function followPickedConversation(pickedKind: string | undefined | null, currentKind: string | undefined): string | undefined {
|
|
190
|
+
if (!pickedKind || pickedKind === currentKind) return undefined
|
|
191
|
+
return pickedKind
|
|
192
|
+
}
|
|
@@ -32,3 +32,23 @@ describe('the panel does not raise a second tab axis', () => {
|
|
|
32
32
|
expect(bare('assistant-session-toolbar.ts')).toContain('role="tablist"')
|
|
33
33
|
})
|
|
34
34
|
})
|
|
35
|
+
|
|
36
|
+
/*
|
|
37
|
+
* 첨부는 **되는 것만** 권한다 — ADR-0071 보탬(2026-09-18).
|
|
38
|
+
*
|
|
39
|
+
* `.json` · `.txt` · `.csv` 도 받고 있었는데, 그 파일들은 실려만 가고 모델에 붙는 코드가 없었다. 사람이
|
|
40
|
+
* CSV 를 붙이고 물으면 파일 칩이 뜨고 답도 왔다 — 그 답은 파일을 본 적이 없고 오류도 안 났다.
|
|
41
|
+
* 이미지는 `hostContext.image` 로 사람 턴에 붙는 길이 있다.
|
|
42
|
+
*/
|
|
43
|
+
describe('the attach control offers only what has a path', () => {
|
|
44
|
+
it('★ 이미지만 받는다 — 읽히지 않는 것을 화면이 권하지 않는다', () => {
|
|
45
|
+
/*
|
|
46
|
+
* 여기서는 주석을 안 벗긴다. `image/*` 자체가 `/*` 를 품고 있어서, 주석 벗기개가 그 자리를 주석의
|
|
47
|
+
* 시작으로 읽고 다음 `*` + `/` 까지 지워 버린다 — 값이 사라지고 시험은 「없다」고 답한다.
|
|
48
|
+
* 이 값은 주석에 적힐 일이 없으므로 원문에서 그대로 읽는다.
|
|
49
|
+
*/
|
|
50
|
+
const accept = readFileSync(join(__dirname, 'assistant-chat.ts'), 'utf-8').match(/accept="([^"]*)"/)
|
|
51
|
+
|
|
52
|
+
expect(accept?.[1]).toBe('image/*')
|
|
53
|
+
})
|
|
54
|
+
})
|
|
@@ -1,4 +1,17 @@
|
|
|
1
|
-
export interface AssistantSession {
|
|
1
|
+
export interface AssistantSession {
|
|
2
|
+
id: string
|
|
3
|
+
name?: string
|
|
4
|
+
/**
|
|
5
|
+
* 어느 대화인가 — 등록된 모드의 kind (ADR-0072 결정 3).
|
|
6
|
+
*
|
|
7
|
+
* 목록은 한 대상의 대화를 **모두** 보여 준다. 그 대상 위에 대화가 둘 설 수 있으므로(A17), 고른 것이
|
|
8
|
+
* 지금 열려 있는 대화와 다를 수 있다. 그때 화면은 그 대화로 따라가야 한다 — 안 그러면 문이 거절하고
|
|
9
|
+
* (`session-kind-mismatch`), 목록은 눌리는데 안 열리는 목록이 된다.
|
|
10
|
+
*
|
|
11
|
+
* 칸이 서기 전에 난 세션에는 값이 없다. 그때는 지금 대화로 두고, 서버가 다음 턴에 찍는다.
|
|
12
|
+
*/
|
|
13
|
+
kind?: string
|
|
14
|
+
}
|
|
2
15
|
export interface AssistantSessionTarget {
|
|
3
16
|
/** Account/domain boundary supplied by the authenticated host. */
|
|
4
17
|
scope: string
|
|
@@ -16,7 +16,7 @@ export const assistantSessionTransport: AssistantSessionTransport = {
|
|
|
16
16
|
},
|
|
17
17
|
async list(target) {
|
|
18
18
|
const result = await client.query({
|
|
19
|
-
query: gql`query($type:String!,$id:String!){chatSessionInbox(anchorType:$type,anchorId:$id,mineOnly:true,limit:100){id name}}`,
|
|
19
|
+
query: gql`query($type:String!,$id:String!){chatSessionInbox(anchorType:$type,anchorId:$id,mineOnly:true,limit:100){id name kind}}`,
|
|
20
20
|
variables: { type: target.anchorType, id: target.anchorId }, fetchPolicy: 'no-cache'
|
|
21
21
|
})
|
|
22
22
|
if (result.errors?.length) throw new Error(result.errors[0].message)
|
|
@@ -32,7 +32,7 @@ export const assistantSessionTransport: AssistantSessionTransport = {
|
|
|
32
32
|
},
|
|
33
33
|
async create(target) {
|
|
34
34
|
const result = await client.mutate({
|
|
35
|
-
mutation: gql`mutation($type:String!,$id:String!){createAISession(anchorType:$type,anchorId:$id){id name}}`,
|
|
35
|
+
mutation: gql`mutation($type:String!,$id:String!){createAISession(anchorType:$type,anchorId:$id){id name kind}}`,
|
|
36
36
|
variables: { type: target.anchorType, id: target.anchorId }
|
|
37
37
|
})
|
|
38
38
|
if (result.errors?.length) throw new Error(result.errors[0].message)
|
|
@@ -326,6 +326,18 @@ export declare class OxAssistantChat extends LitElement {
|
|
|
326
326
|
* (서버에 칸을 만들지 않는다 — A17 ①). 세션 자체는 자기가 어느 대화였는지 이미 안다.
|
|
327
327
|
*/
|
|
328
328
|
private onModePick;
|
|
329
|
+
/**
|
|
330
|
+
* **고르는 것은 청하는 일이다** — `mode` 를 여기서 바꾸지 않는다.
|
|
331
|
+
*
|
|
332
|
+
* host 는 렌더마다 `mode` 를 다시 내려 준다. 패널이 자기 손으로 고쳐 두면 다음 렌더가 그것을 덮고,
|
|
333
|
+
* 더 나쁜 것은 host 가 **자기가 무엇을 열었는지 모르는 채로 남는다**는 것이다. host 는 대화의 세션을
|
|
334
|
+
* 대상별로 들고 있으므로, 대화가 바뀐 것을 모르면 앞 대화의 세션을 그대로 넘긴다 — 그러면 새 말이 앞
|
|
335
|
+
* 대화에 붙는다(2026-09-18 Figure 레인이 :3300 에서 본 것이 이것이다).
|
|
336
|
+
*
|
|
337
|
+
* 그래서 주인은 하나다: host 가 `mode` 를 갖고, 패널은 「이것으로 바꿔 달라」고 말한다. 기억은 패널이
|
|
338
|
+
* 진다(브라우저마다 다른 값이라 host 가 질 것이 아니다) — 기억한 것을 되살릴 때도 같은 청으로 나간다.
|
|
339
|
+
*/
|
|
340
|
+
private askHostForMode;
|
|
329
341
|
/** 이 subject 에서 지난번에 고른 대화 — 없으면 undefined. */
|
|
330
342
|
private rememberedMode;
|
|
331
343
|
private loadModeView;
|
|
@@ -129,6 +129,8 @@ const ASSISTANT_MODE_QUERY = gql `
|
|
|
129
129
|
catalogEntries
|
|
130
130
|
proposalChoices
|
|
131
131
|
placeholder
|
|
132
|
+
intro
|
|
133
|
+
examples
|
|
132
134
|
footerNotice
|
|
133
135
|
stagedNotice
|
|
134
136
|
}
|
|
@@ -387,6 +389,22 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
|
|
|
387
389
|
this.proposalSentAt = new Map();
|
|
388
390
|
}
|
|
389
391
|
static { OxAssistantChat_1 = this; }
|
|
392
|
+
/*
|
|
393
|
+
* ── 왜 이미지만 받나 ──────────────────────────────────────────────────────
|
|
394
|
+
* `.json` · `.txt` · `.csv` 도 받고 있었다. 그 파일들은 `hostContext.attachments` 로 실려 갔고
|
|
395
|
+
* **그것을 읽어 모델에 붙이는 코드가 없었다.** 사람이 CSV 를 붙이고 「이 표 좀 봐줘」라고 하면 화면에
|
|
396
|
+
* 파일 칩이 뜨고 답도 왔다. 그 답은 파일을 본 적이 없고, 오류도 안 났다(2026-09-18 표면 조사).
|
|
397
|
+
*
|
|
398
|
+
* 이미지는 길이 있다 — `assistant-chat-resolver.ts` 가 `hostContext.image` 를 마지막 사람 턴에
|
|
399
|
+
* 붙인다. 그래서 되는 것만 남기고 좁혔다. 못 하는 일을 화면이 권하지 않는다.
|
|
400
|
+
*
|
|
401
|
+
* ── 글 파일을 청하는 제품이 설 때의 모양 (수석 아키텍트, ADR-0071 보탬) ───
|
|
402
|
+
* 이미지와 **같은 자리**(사람 턴)에 붙인다. 크기 상한은 모드가 선언하고, 넘으면 보내기 전에 사실 한
|
|
403
|
+
* 줄로 거절한다 — 조용히 자르지 않는다. 비용은 사용량의 축이다(ADR-0061 결정 6).
|
|
404
|
+
*
|
|
405
|
+
* `attachments` 짐 자체는 그대로 둔다: figure 의 도구가 거기서 이미지를 찾는다
|
|
406
|
+
* (`figure-tools.ts` — `ctx.host.image` 가 없을 때의 갈래).
|
|
407
|
+
*/
|
|
390
408
|
async onAttachFiles(files) {
|
|
391
409
|
if (!files || files.length === 0)
|
|
392
410
|
return;
|
|
@@ -567,7 +585,6 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
|
|
|
567
585
|
onModePick(kind) {
|
|
568
586
|
if (!this.mode || kind === this.mode.kind)
|
|
569
587
|
return;
|
|
570
|
-
this.mode = { ...this.mode, kind };
|
|
571
588
|
try {
|
|
572
589
|
if (this.subject)
|
|
573
590
|
localStorage.setItem(`assistant-mode:${this.subject}`, kind);
|
|
@@ -575,6 +592,20 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
|
|
|
575
592
|
catch {
|
|
576
593
|
/* 저장이 막힌 브라우저에서도 고르기 자체는 된다 — 다음에 다시 고르면 될 뿐이다. */
|
|
577
594
|
}
|
|
595
|
+
this.askHostForMode(kind);
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* **고르는 것은 청하는 일이다** — `mode` 를 여기서 바꾸지 않는다.
|
|
599
|
+
*
|
|
600
|
+
* host 는 렌더마다 `mode` 를 다시 내려 준다. 패널이 자기 손으로 고쳐 두면 다음 렌더가 그것을 덮고,
|
|
601
|
+
* 더 나쁜 것은 host 가 **자기가 무엇을 열었는지 모르는 채로 남는다**는 것이다. host 는 대화의 세션을
|
|
602
|
+
* 대상별로 들고 있으므로, 대화가 바뀐 것을 모르면 앞 대화의 세션을 그대로 넘긴다 — 그러면 새 말이 앞
|
|
603
|
+
* 대화에 붙는다(2026-09-18 Figure 레인이 :3300 에서 본 것이 이것이다).
|
|
604
|
+
*
|
|
605
|
+
* 그래서 주인은 하나다: host 가 `mode` 를 갖고, 패널은 「이것으로 바꿔 달라」고 말한다. 기억은 패널이
|
|
606
|
+
* 진다(브라우저마다 다른 값이라 host 가 질 것이 아니다) — 기억한 것을 되살릴 때도 같은 청으로 나간다.
|
|
607
|
+
*/
|
|
608
|
+
askHostForMode(kind) {
|
|
578
609
|
this.dispatchEvent(new CustomEvent('assistant-mode-change', { detail: { kind, subject: this.subject }, bubbles: true, composed: true }));
|
|
579
610
|
}
|
|
580
611
|
/** 이 subject 에서 지난번에 고른 대화 — 없으면 undefined. */
|
|
@@ -2219,7 +2250,7 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
|
|
|
2219
2250
|
return;
|
|
2220
2251
|
const kind = pickInitialMode(this.mode.kind, this.modeChoices, this.rememberedMode(this.subject));
|
|
2221
2252
|
if (kind && kind !== this.mode.kind)
|
|
2222
|
-
this.
|
|
2253
|
+
this.askHostForMode(kind);
|
|
2223
2254
|
}
|
|
2224
2255
|
updated(changed) {
|
|
2225
2256
|
/*
|
|
@@ -2578,7 +2609,7 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
|
|
|
2578
2609
|
id="chat-file-input"
|
|
2579
2610
|
type="file"
|
|
2580
2611
|
multiple
|
|
2581
|
-
accept="image
|
|
2612
|
+
accept="image/*"
|
|
2582
2613
|
style="display: none"
|
|
2583
2614
|
@change=${(e) => {
|
|
2584
2615
|
const input = e.target;
|
|
@@ -2592,7 +2623,7 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
|
|
|
2592
2623
|
type="button"
|
|
2593
2624
|
class="action"
|
|
2594
2625
|
@click=${() => this.renderRoot.querySelector('#chat-file-input')?.click()}
|
|
2595
|
-
title=${i18next.t('ai-assistant.tooltip.attach-file', { defaultValue: '
|
|
2626
|
+
title=${i18next.t('ai-assistant.tooltip.attach-file', { defaultValue: 'attach an image (drag & drop or paste supported)' })}>
|
|
2596
2627
|
<md-icon>attach_file</md-icon>
|
|
2597
2628
|
${i18next.t('ai-assistant.button.attach', { defaultValue: 'Attach' })}
|
|
2598
2629
|
</button>
|
|
@@ -3061,6 +3092,12 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
|
|
|
3061
3092
|
* 읽었고, 그러면 호스트 값을 한쪽만 반영하는 상태가 만들어질 수 있었다.
|
|
3062
3093
|
*/
|
|
3063
3094
|
get exampleGroups() {
|
|
3095
|
+
/*
|
|
3096
|
+
* 대화가 자기 예시를 가지면 그것이 먼저다 — 자리표·`/` 와 같은 차례(ADR-0061 결정 7). host 속성은
|
|
3097
|
+
* 대화를 모르므로, 한 화면에 대화가 둘 서면 한쪽에 맞는 글이 다른 쪽에도 뜬다.
|
|
3098
|
+
*/
|
|
3099
|
+
if (this.modeView?.examples?.length)
|
|
3100
|
+
return this.modeView.examples;
|
|
3064
3101
|
if (this.examples?.length)
|
|
3065
3102
|
return this.examples;
|
|
3066
3103
|
return OxAssistantChat_1.EXAMPLE_GROUPS.map(group => ({
|
|
@@ -3201,8 +3238,13 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
|
|
|
3201
3238
|
<div class="empty">
|
|
3202
3239
|
<div class="header">
|
|
3203
3240
|
<md-icon class="icon">auto_awesome</md-icon>
|
|
3204
|
-
|
|
3205
|
-
<div class="
|
|
3241
|
+
<!-- 대화가 자기 글을 가지면 그것이 먼저다 — host 속성은 대화를 모른다. -->
|
|
3242
|
+
<div class="title">
|
|
3243
|
+
${this.modeView?.intro?.title ?? this.intro?.title ?? i18next.t('ai-assistant.text.empty-title')}
|
|
3244
|
+
</div>
|
|
3245
|
+
<div class="subtitle">
|
|
3246
|
+
${this.modeView?.intro?.subtitle ?? this.intro?.subtitle ?? i18next.t('ai-assistant.text.empty-subtitle')}
|
|
3247
|
+
</div>
|
|
3206
3248
|
</div>
|
|
3207
3249
|
|
|
3208
3250
|
${this.triggerChips.length > 0
|