@things-factory/ai-assistant 10.1.26 → 10.1.28
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 +653 -59
- package/client/components/assistant-mode.test.ts +86 -55
- package/client/components/assistant-mode.ts +124 -82
- package/client/components/chat-echo-dedup.test.ts +23 -0
- package/client/components/chat-echo-dedup.ts +29 -3
- package/client/components/chat-input-builder.test.ts +20 -20
- package/client/components/chat-input-builder.ts +16 -14
- package/client/components/chat-proposal-survival.test.ts +73 -0
- package/client/components/mention-popup.ts +31 -8
- package/client/components/panel-aria.test.ts +34 -0
- package/client/components/proposal-outcome-line.ts +33 -0
- package/client/utils/assistant-session-controller.test.ts +75 -0
- package/client/utils/assistant-session-controller.ts +23 -0
- package/client/utils/assistant-session-transport.ts +11 -3
- package/dist-client/components/assistant-chat.d.ts +128 -13
- package/dist-client/components/assistant-chat.js +613 -67
- package/dist-client/components/assistant-chat.js.map +1 -1
- package/dist-client/components/assistant-mode.d.ts +80 -68
- package/dist-client/components/assistant-mode.js +63 -19
- package/dist-client/components/assistant-mode.js.map +1 -1
- package/dist-client/components/assistant-mode.test.js +74 -53
- package/dist-client/components/assistant-mode.test.js.map +1 -1
- package/dist-client/components/chat-echo-dedup.d.ts +2 -0
- package/dist-client/components/chat-echo-dedup.js +27 -4
- package/dist-client/components/chat-echo-dedup.js.map +1 -1
- package/dist-client/components/chat-echo-dedup.test.js +20 -0
- package/dist-client/components/chat-echo-dedup.test.js.map +1 -1
- package/dist-client/components/chat-input-builder.d.ts +12 -7
- package/dist-client/components/chat-input-builder.js +8 -10
- package/dist-client/components/chat-input-builder.js.map +1 -1
- package/dist-client/components/chat-input-builder.test.js +17 -18
- package/dist-client/components/chat-input-builder.test.js.map +1 -1
- package/dist-client/components/chat-proposal-survival.test.d.ts +1 -0
- package/dist-client/components/chat-proposal-survival.test.js +61 -0
- package/dist-client/components/chat-proposal-survival.test.js.map +1 -0
- package/dist-client/components/mention-popup.js +28 -8
- package/dist-client/components/mention-popup.js.map +1 -1
- package/dist-client/components/panel-aria.test.d.ts +1 -0
- package/dist-client/components/panel-aria.test.js +31 -0
- package/dist-client/components/panel-aria.test.js.map +1 -0
- package/dist-client/components/proposal-outcome-line.d.ts +9 -0
- package/dist-client/components/proposal-outcome-line.js +15 -0
- package/dist-client/components/proposal-outcome-line.js.map +1 -0
- 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 +16 -0
- package/dist-client/utils/assistant-session-controller.js.map +1 -1
- package/dist-client/utils/assistant-session-controller.test.d.ts +1 -0
- package/dist-client/utils/assistant-session-controller.test.js +56 -0
- package/dist-client/utils/assistant-session-controller.test.js.map +1 -0
- package/dist-client/utils/assistant-session-transport.js +11 -3
- package/dist-client/utils/assistant-session-transport.js.map +1 -1
- package/dist-server/service/assistant-capability-resolver.d.ts +17 -0
- package/dist-server/service/assistant-capability-resolver.js +88 -0
- package/dist-server/service/assistant-capability-resolver.js.map +1 -0
- package/dist-server/service/assistant-chat-resolver.d.ts +6 -6
- package/dist-server/service/assistant-chat-resolver.js +166 -44
- package/dist-server/service/assistant-chat-resolver.js.map +1 -1
- package/dist-server/service/assistant-mode/assistant-mode-resolver.d.ts +25 -0
- package/dist-server/service/assistant-mode/assistant-mode-resolver.js +140 -0
- package/dist-server/service/assistant-mode/assistant-mode-resolver.js.map +1 -0
- package/dist-server/service/assistant-mode/index.d.ts +9 -0
- package/dist-server/service/assistant-mode/index.js +19 -0
- package/dist-server/service/assistant-mode/index.js.map +1 -0
- package/dist-server/service/assistant-mode/registry.d.ts +18 -0
- package/dist-server/service/assistant-mode/registry.js +136 -0
- package/dist-server/service/assistant-mode/registry.js.map +1 -0
- package/dist-server/service/assistant-mode/resolve.d.ts +33 -0
- package/dist-server/service/assistant-mode/resolve.js +60 -0
- package/dist-server/service/assistant-mode/resolve.js.map +1 -0
- package/dist-server/service/assistant-mode/types.d.ts +99 -0
- package/dist-server/service/assistant-mode/types.js +12 -0
- package/dist-server/service/assistant-mode/types.js.map +1 -0
- package/dist-server/service/chat-message/carried-turns.d.ts +48 -0
- package/dist-server/service/chat-message/carried-turns.js +80 -0
- package/dist-server/service/chat-message/carried-turns.js.map +1 -0
- package/dist-server/service/chat-message/chat-message-subscription.d.ts +1 -1
- package/dist-server/service/chat-message/chat-message-subscription.js +24 -6
- package/dist-server/service/chat-message/chat-message-subscription.js.map +1 -1
- package/dist-server/service/chat-message/chat-message.d.ts +32 -0
- package/dist-server/service/chat-message/chat-message.js +17 -0
- package/dist-server/service/chat-message/chat-message.js.map +1 -1
- package/dist-server/service/chat-message/door-access.d.ts +15 -0
- package/dist-server/service/chat-message/door-access.js +26 -0
- package/dist-server/service/chat-message/door-access.js.map +1 -0
- package/dist-server/service/chat-message/fold-session-history.d.ts +30 -0
- package/dist-server/service/chat-message/fold-session-history.js +133 -0
- package/dist-server/service/chat-message/fold-session-history.js.map +1 -0
- package/dist-server/service/chat-message/llm-history.d.ts +7 -0
- package/dist-server/service/chat-message/llm-history.js +23 -11
- package/dist-server/service/chat-message/llm-history.js.map +1 -1
- package/dist-server/service/chat-message/message-doors.d.ts +29 -0
- package/dist-server/service/chat-message/message-doors.js +107 -0
- package/dist-server/service/chat-message/message-doors.js.map +1 -0
- package/dist-server/service/chat-message/proposal-outcome.d.ts +18 -0
- package/dist-server/service/chat-message/proposal-outcome.js +31 -0
- package/dist-server/service/chat-message/proposal-outcome.js.map +1 -0
- package/dist-server/service/chat-message/reader-gate.d.ts +3 -0
- package/dist-server/service/chat-message/reader-gate.js +37 -0
- package/dist-server/service/chat-message/reader-gate.js.map +1 -0
- package/dist-server/service/chat-message/room-message.d.ts +4 -0
- package/dist-server/service/chat-message/room-message.js +25 -0
- package/dist-server/service/chat-message/room-message.js.map +1 -0
- package/dist-server/service/chat-session/chat-session.d.ts +24 -0
- package/dist-server/service/chat-session/chat-session.js +26 -2
- package/dist-server/service/chat-session/chat-session.js.map +1 -1
- package/dist-server/service/chat-session/session-inbox.d.ts +4 -1
- package/dist-server/service/chat-session/session-inbox.js +3 -2
- package/dist-server/service/chat-session/session-inbox.js.map +1 -1
- package/dist-server/service/chat-session/session-visibility.d.ts +15 -0
- package/dist-server/service/chat-session/session-visibility.js +28 -0
- package/dist-server/service/chat-session/session-visibility.js.map +1 -0
- package/dist-server/service/chat-session/station-session-guard.d.ts +1 -5
- package/dist-server/service/chat-session/station-session-guard.js +7 -2
- package/dist-server/service/chat-session/station-session-guard.js.map +1 -1
- package/dist-server/service/chat-session-participant/mention-invite.d.ts +29 -0
- package/dist-server/service/chat-session-participant/mention-invite.js +103 -0
- package/dist-server/service/chat-session-participant/mention-invite.js.map +1 -0
- package/dist-server/service/chat-session-resolver.d.ts +15 -0
- package/dist-server/service/chat-session-resolver.js +267 -15
- package/dist-server/service/chat-session-resolver.js.map +1 -1
- package/dist-server/service/grounding-sources.d.ts +14 -0
- package/dist-server/service/grounding-sources.js +26 -0
- package/dist-server/service/grounding-sources.js.map +1 -0
- package/dist-server/service/index.d.ts +8 -1
- package/dist-server/service/index.js +19 -0
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/service/request-translator.d.ts +2 -0
- package/dist-server/service/request-translator.js +20 -0
- package/dist-server/service/request-translator.js.map +1 -0
- package/dist-server/service/testing/conversation-script.d.ts +123 -0
- package/dist-server/service/testing/conversation-script.js +202 -0
- package/dist-server/service/testing/conversation-script.js.map +1 -0
- package/dist-server/service/testing/mode-conformance.d.ts +40 -0
- package/dist-server/service/testing/mode-conformance.js +90 -0
- package/dist-server/service/testing/mode-conformance.js.map +1 -0
- package/dist-server/tool-registry-boot-report.d.ts +3 -0
- package/dist-server/tool-registry-boot-report.js +25 -2
- 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/explicit-graphql-types.test.ts +56 -0
- package/server/service/assistant-capability-resolver.ts +66 -0
- package/server/service/assistant-chat-resolver.ts +184 -42
- package/server/service/assistant-mode/assistant-mode-resolver.ts +102 -0
- package/server/service/assistant-mode/index.ts +16 -0
- package/server/service/assistant-mode/registry.test.ts +105 -0
- package/server/service/assistant-mode/registry.ts +147 -0
- package/server/service/assistant-mode/resolve.test.ts +78 -0
- package/server/service/assistant-mode/resolve.ts +96 -0
- package/server/service/assistant-mode/subject-defaults.test.ts +86 -0
- package/server/service/assistant-mode/types.ts +103 -0
- package/server/service/chat-message/carried-turns.test.ts +69 -0
- package/server/service/chat-message/carried-turns.ts +99 -0
- package/server/service/chat-message/chat-message-subscription.ts +20 -4
- package/server/service/chat-message/chat-message.ts +44 -0
- package/server/service/chat-message/door-access.ts +45 -0
- package/server/service/chat-message/fold-session-history.test.ts +98 -0
- package/server/service/chat-message/fold-session-history.ts +115 -0
- package/server/service/chat-message/llm-history.test.ts +58 -7
- package/server/service/chat-message/llm-history.ts +30 -11
- package/server/service/chat-message/message-doors.test.ts +87 -0
- package/server/service/chat-message/message-doors.ts +117 -0
- package/server/service/chat-message/proposal-outcome.test.ts +48 -0
- package/server/service/chat-message/proposal-outcome.ts +42 -0
- package/server/service/chat-message/reader-gate.ts +42 -0
- package/server/service/chat-message/room-message.test.ts +21 -0
- package/server/service/chat-message/room-message.ts +22 -0
- package/server/service/chat-session/chat-session.ts +31 -1
- package/server/service/chat-session/session-inbox.test.ts +18 -0
- package/server/service/chat-session/session-inbox.ts +9 -2
- package/server/service/chat-session/session-visibility.test.ts +36 -0
- package/server/service/chat-session/session-visibility.ts +26 -0
- package/server/service/chat-session/station-session-guard.ts +10 -2
- package/server/service/chat-session-participant/mention-invite.test.ts +125 -0
- package/server/service/chat-session-participant/mention-invite.ts +121 -0
- package/server/service/chat-session-resolver.ts +272 -15
- package/server/service/grounding-sources.test.ts +55 -0
- package/server/service/grounding-sources.ts +46 -0
- package/server/service/index.ts +19 -0
- package/server/service/request-translator.test.ts +45 -0
- package/server/service/request-translator.ts +33 -0
- package/server/service/station-session-guard.test.ts +12 -5
- package/server/service/testing/conversation-scenario.test.ts +163 -0
- package/server/service/testing/conversation-script.test.ts +131 -0
- package/server/service/testing/conversation-script.ts +288 -0
- package/server/service/testing/mode-conformance.ts +107 -0
- package/server/tool-registry-boot-report.ts +25 -2
- package/test/assistant-chat-call-sites.test.ts +82 -0
- package/translations/en.json +44 -2
- package/translations/ja.json +44 -2
- package/translations/ko.json +44 -2
- package/translations/ms.json +44 -2
- package/translations/zh.json +44 -2
|
@@ -43,7 +43,8 @@ import { buildMentionCandidates, buildCatalogCandidates, buildSlashCandidates, b
|
|
|
43
43
|
* 뜨지 않는 것이 맞다.
|
|
44
44
|
*/
|
|
45
45
|
import { chatDefaults } from './chat-defaults.js';
|
|
46
|
-
import { isDifferentConversation, resolveConversation } from './assistant-mode.js';
|
|
46
|
+
import { isDifferentConversation, resolveConversation, pickInitialMode } from './assistant-mode.js';
|
|
47
|
+
import { parseProposalOutcomeLine } from './proposal-outcome-line.js';
|
|
47
48
|
import { availableTriggers } from './chat-triggers.js';
|
|
48
49
|
function formatFileSize(bytes) {
|
|
49
50
|
if (!bytes || bytes < 1024)
|
|
@@ -91,9 +92,75 @@ const ASSISTANT_CHAT_MUTATION = gql `
|
|
|
91
92
|
offeredTools
|
|
92
93
|
proposals
|
|
93
94
|
groundingWarnings
|
|
95
|
+
historyFolded
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
`;
|
|
99
|
+
/*
|
|
100
|
+
* What this conversation offers on screen, in this person's language.
|
|
101
|
+
*
|
|
102
|
+
* The `/` sentences, the `@` catalog and the two notices are declared with the mode on the server
|
|
103
|
+
* (ADR-0061 decision 7) and translated there, so the panel and the door cannot describe different
|
|
104
|
+
* conversations, and a host does not carry a second copy of the words.
|
|
105
|
+
*/
|
|
106
|
+
/*
|
|
107
|
+
* 이 화면에 설 수 있는 대화들 — ADR-0072 A17 ①.
|
|
108
|
+
*
|
|
109
|
+
* 화면은 자기가 무엇을 보여 주는지만 말하고, 그 위에 어느 대화가 서는지는 앱 서버가 답한다.
|
|
110
|
+
*/
|
|
111
|
+
const ASSISTANT_MODES_QUERY = gql `
|
|
112
|
+
query AssistantModes($subject: String!) {
|
|
113
|
+
assistantModes(subject: $subject) {
|
|
114
|
+
kind
|
|
115
|
+
label
|
|
116
|
+
isDefault
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
`;
|
|
120
|
+
const ASSISTANT_MODE_QUERY = gql `
|
|
121
|
+
query AssistantMode($kind: String!) {
|
|
122
|
+
assistantMode(kind: $kind) {
|
|
123
|
+
kind
|
|
124
|
+
slashTemplates {
|
|
125
|
+
name
|
|
126
|
+
description
|
|
127
|
+
template
|
|
128
|
+
}
|
|
129
|
+
catalogEntries
|
|
130
|
+
proposalChoices
|
|
131
|
+
placeholder
|
|
132
|
+
footerNotice
|
|
133
|
+
stagedNotice
|
|
94
134
|
}
|
|
95
135
|
}
|
|
96
136
|
`;
|
|
137
|
+
/*
|
|
138
|
+
* 방에 말하는 문 — 사람에게 하는 말이고 모델을 부르지 않는다(ADR-0070 A15).
|
|
139
|
+
*
|
|
140
|
+
* 이 문이 서기 전에는 화면에서 나가는 모든 말이 어시스턴트를 향했다. 옆 사람에게 한마디 하려면
|
|
141
|
+
* 어시스턴트가 반드시 대답했고, 그 대답이 다시 대화에 남았다.
|
|
142
|
+
*/
|
|
143
|
+
/*
|
|
144
|
+
* `@` 가 고를 수 있는 사람들 — 이 도메인의 사용자.
|
|
145
|
+
*
|
|
146
|
+
* 호스트가 `userProvider` 를 주지 않으면 여기서 직접 묻는다. 주지 않는 것이 기본이었고, 그래서 앱 셋
|
|
147
|
+
* 어디에서도 `@이름` 을 **고를 수가 없었다** — 멘션이 사람을 불러들이는 기전(ADR-0071 결정 1)을 세워
|
|
148
|
+
* 놓고 부르는 자리가 없는 상태였다. 이 문은 `ai-assistant` 의 것이고 대화창이 그 문의 화면이다.
|
|
149
|
+
*/
|
|
150
|
+
const DOMAIN_USERS_FOR_MENTION_QUERY = gql `
|
|
151
|
+
query DomainUsersForMention($query: String) {
|
|
152
|
+
domainUsersForMention(query: $query) {
|
|
153
|
+
id
|
|
154
|
+
name
|
|
155
|
+
email
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
`;
|
|
159
|
+
const SAY_TO_ROOM_MUTATION = gql `
|
|
160
|
+
mutation SayToRoom($sessionId: String!, $content: String!) {
|
|
161
|
+
sayToRoom(sessionId: $sessionId, content: $content)
|
|
162
|
+
}
|
|
163
|
+
`;
|
|
97
164
|
const CHAT_MESSAGES_QUERY = gql `
|
|
98
165
|
query ChatMessages($sessionId: String!, $limit: Int, $offset: Int) {
|
|
99
166
|
chatMessages(sessionId: $sessionId, limit: $limit, offset: $offset) {
|
|
@@ -104,6 +171,7 @@ const CHAT_MESSAGES_QUERY = gql `
|
|
|
104
171
|
toolUsagesJson
|
|
105
172
|
groundingWarningsJson
|
|
106
173
|
createdAt
|
|
174
|
+
redacted
|
|
107
175
|
creator {
|
|
108
176
|
name
|
|
109
177
|
email
|
|
@@ -123,6 +191,7 @@ const CHAT_MESSAGE_SUBSCRIPTION = gql `
|
|
|
123
191
|
toolUsagesJson
|
|
124
192
|
groundingWarningsJson
|
|
125
193
|
createdAt
|
|
194
|
+
redacted
|
|
126
195
|
creator {
|
|
127
196
|
name
|
|
128
197
|
email
|
|
@@ -186,22 +255,6 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
|
|
|
186
255
|
this.selectedRefids = [];
|
|
187
256
|
/** 코어 보드 편집 도구 사용 여부. 기본 true. 보드를 고칠 이유가 없는 면에서 false. */
|
|
188
257
|
this.boardTools = true;
|
|
189
|
-
/*
|
|
190
|
-
* Which server operation this surface talks to. A closed set of two, named after the operations
|
|
191
|
-
* themselves so a reader can grep either way.
|
|
192
|
-
*
|
|
193
|
-
* boardAIChat board authoring — lives in @things-factory/board-ai, carries the board model,
|
|
194
|
-
* returns patches and scene actions
|
|
195
|
-
* assistantChat a surface with no document of its own — lives here, runs registered tools only
|
|
196
|
-
*
|
|
197
|
-
* Default stays `boardAIChat`: every existing consumer is a board surface, and a default that
|
|
198
|
-
* changed under them would move their conversation to a door with no board tools behind it.
|
|
199
|
-
*
|
|
200
|
-
* A host that has no board must pass `assistantChat` **and** `systemPrompt`. Until it does, the
|
|
201
|
-
* chat calls a mutation that lives in a package it may not even load — which is how
|
|
202
|
-
* operato-figure got `Cannot query field "boardAIChat" on type "Mutation"` (2026-09-11).
|
|
203
|
-
*/
|
|
204
|
-
this.chatEndpoint = 'boardAIChat';
|
|
205
258
|
/**
|
|
206
259
|
* 라이브 상태를 다루는 대화면인가 — true 면 **첫 턴에 도구 호출을 강제**한다.
|
|
207
260
|
*
|
|
@@ -209,7 +262,6 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
|
|
|
209
262
|
* 그건 도구를 부르지 않고 답한 경우이므로, 조회를 먼저 하게 만들어 원천을 막는다.
|
|
210
263
|
*/
|
|
211
264
|
this.requireGroundingTools = false;
|
|
212
|
-
this.placeholder = '자연어로 명령하세요 (Shift+Enter 줄바꿈, Enter 전송)';
|
|
213
265
|
this.lines = [];
|
|
214
266
|
this.input = '';
|
|
215
267
|
/** 되돌려진 patch id 들 — 한 번 revert 한 patch 는 버튼 비활성 */
|
|
@@ -260,6 +312,10 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
|
|
|
260
312
|
* textarea 에는 @username 만 보이지만 server 한테는 정확한 userId 전달.
|
|
261
313
|
* send 시 mutation input.userMentions 로 함께 전송. */
|
|
262
314
|
this.pickedUserMentions = new Map();
|
|
315
|
+
this.modeViewToken = 0;
|
|
316
|
+
/** 이 subject 에 설 수 있는 대화들 — 기본이 먼저. 하나뿐이면 고르개를 그리지 않는다. */
|
|
317
|
+
this.modeChoices = [];
|
|
318
|
+
this.modeChoicesToken = 0;
|
|
263
319
|
/*
|
|
264
320
|
* The `@` and `/` candidates, built once per mode.
|
|
265
321
|
*
|
|
@@ -318,6 +374,17 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
|
|
|
318
374
|
this.foldNoticeDismissed = true;
|
|
319
375
|
this.dispatchEvent(new CustomEvent('assistant-start-new-session', { bubbles: true, composed: true }));
|
|
320
376
|
};
|
|
377
|
+
/**
|
|
378
|
+
* 실행은 호스트가 한다 — 이 컴포넌트는 도메인(트윈 명령 채널)을 모른다.
|
|
379
|
+
* 확인 대화·명령 전송·결과 기록은 호스트의 몫이고, 여기서는 중복 클릭만 막는다.
|
|
380
|
+
*/
|
|
381
|
+
/**
|
|
382
|
+
* 누른 시각 — 「보내는 중」 옆의 경과 시간용.
|
|
383
|
+
*
|
|
384
|
+
* 시간으로 실패를 판정하지 않는다(ADR-0070 A12-a 결정 ②). 아는 사실은 「아직 답을 받지 못했다」
|
|
385
|
+
* 하나이고, 시간이 지났다고 실패로 바꾸면 그것이 지어낸 원인이다.
|
|
386
|
+
*/
|
|
387
|
+
this.proposalSentAt = new Map();
|
|
321
388
|
}
|
|
322
389
|
static { OxAssistantChat_1 = this; }
|
|
323
390
|
async onAttachFiles(files) {
|
|
@@ -422,10 +489,11 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
|
|
|
422
489
|
The screen still shows nothing either way. There is no sentence to show, and inventing one is
|
|
423
490
|
what this whole correction removed.
|
|
424
491
|
*/
|
|
425
|
-
if (!this.conversation.
|
|
492
|
+
if (!this.conversation.stagedNotice && !OxAssistantChat_1.warnedMissingStagedNotice) {
|
|
426
493
|
OxAssistantChat_1.warnedMissingStagedNotice = true;
|
|
427
|
-
console.warn('[assistant-chat] a host reported a proposal staged but
|
|
428
|
-
'
|
|
494
|
+
console.warn('[assistant-chat] a host reported a proposal staged but declared no staged notice, so the card can ' +
|
|
495
|
+
'say nothing. Declare `stagedNoticeKey` on the mode (the server translates it), or pass one to ' +
|
|
496
|
+
'`registerChatDefaults` from a host with no mode.');
|
|
429
497
|
}
|
|
430
498
|
this.stagedProposals = new Set(this.stagedProposals).add(this.proposalKey(proposal));
|
|
431
499
|
}
|
|
@@ -437,6 +505,9 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
|
|
|
437
505
|
get _myEmail() {
|
|
438
506
|
return auth.credential?.email;
|
|
439
507
|
}
|
|
508
|
+
get _myUserId() {
|
|
509
|
+
return auth.credential?.id;
|
|
510
|
+
}
|
|
440
511
|
/*
|
|
441
512
|
* ── What this conversation is, resolved ──────────────────────────────────────────────
|
|
442
513
|
*
|
|
@@ -446,11 +517,100 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
|
|
|
446
517
|
*/
|
|
447
518
|
get conversation() {
|
|
448
519
|
return resolveConversation(this.mode, {
|
|
449
|
-
systemPrompt: this.systemPrompt,
|
|
450
|
-
toolCategories: this.toolCategories,
|
|
451
520
|
hostContext: this.hostContext,
|
|
452
|
-
chatEndpoint: this.chatEndpoint
|
|
453
|
-
|
|
521
|
+
chatEndpoint: this.chatEndpoint,
|
|
522
|
+
/* A host with no mode still registers its wording as keys; it is translated here. */
|
|
523
|
+
footerNotice: chatDefaults.footerNoticeKey ? i18next.t(chatDefaults.footerNoticeKey) : undefined,
|
|
524
|
+
stagedNotice: chatDefaults.stagedNoticeKey ? i18next.t(chatDefaults.stagedNoticeKey) : undefined
|
|
525
|
+
}, chatDefaults, this.modeView);
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* 이 화면에 설 수 있는 대화를 서버에 묻는다.
|
|
529
|
+
*
|
|
530
|
+
* 화면이 종류 목록을 손에 들고 있으면, 대화를 하나 더 세운 날 그 화면만 모른 채로 남는다. 목록은
|
|
531
|
+
* 앱 서버의 선언에서 나온다(ADR-0072 A17 ①).
|
|
532
|
+
*/
|
|
533
|
+
async loadModeChoices() {
|
|
534
|
+
const subject = this.subject;
|
|
535
|
+
if (!subject) {
|
|
536
|
+
this.modeChoices = [];
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
const token = ++this.modeChoicesToken;
|
|
540
|
+
try {
|
|
541
|
+
const result = await client.query({
|
|
542
|
+
query: ASSISTANT_MODES_QUERY,
|
|
543
|
+
variables: { subject },
|
|
544
|
+
fetchPolicy: 'cache-first'
|
|
545
|
+
});
|
|
546
|
+
if (token !== this.modeChoicesToken)
|
|
547
|
+
return;
|
|
548
|
+
this.modeChoices = result.data?.assistantModes ?? [];
|
|
549
|
+
}
|
|
550
|
+
catch {
|
|
551
|
+
/*
|
|
552
|
+
* 못 물어봤다고 화면을 막지 않는다 — host 가 준 대화는 그대로 산다. 고르개만 안 나온다.
|
|
553
|
+
* 여기서 오류 문구를 띄우면 고를 것이 하나뿐인 화면에서도 빨간 줄이 뜬다.
|
|
554
|
+
*/
|
|
555
|
+
if (token !== this.modeChoicesToken)
|
|
556
|
+
return;
|
|
557
|
+
this.modeChoices = [];
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* 사람이 다른 대화를 골랐다.
|
|
562
|
+
*
|
|
563
|
+
* 고른 것은 **이 subject 종류에 대해** 기억한다 — 보드를 열 때마다 저작 대화로 돌아가면, 현장을
|
|
564
|
+
* 물으려던 사람은 화면을 열 때마다 같은 손질을 다시 해야 한다. 기억은 이 브라우저에만 남는다
|
|
565
|
+
* (서버에 칸을 만들지 않는다 — A17 ①). 세션 자체는 자기가 어느 대화였는지 이미 안다.
|
|
566
|
+
*/
|
|
567
|
+
onModePick(kind) {
|
|
568
|
+
if (!this.mode || kind === this.mode.kind)
|
|
569
|
+
return;
|
|
570
|
+
this.mode = { ...this.mode, kind };
|
|
571
|
+
try {
|
|
572
|
+
if (this.subject)
|
|
573
|
+
localStorage.setItem(`assistant-mode:${this.subject}`, kind);
|
|
574
|
+
}
|
|
575
|
+
catch {
|
|
576
|
+
/* 저장이 막힌 브라우저에서도 고르기 자체는 된다 — 다음에 다시 고르면 될 뿐이다. */
|
|
577
|
+
}
|
|
578
|
+
this.dispatchEvent(new CustomEvent('assistant-mode-change', { detail: { kind, subject: this.subject }, bubbles: true, composed: true }));
|
|
579
|
+
}
|
|
580
|
+
/** 이 subject 에서 지난번에 고른 대화 — 없으면 undefined. */
|
|
581
|
+
rememberedMode(subject) {
|
|
582
|
+
try {
|
|
583
|
+
return localStorage.getItem(`assistant-mode:${subject}`) ?? undefined;
|
|
584
|
+
}
|
|
585
|
+
catch {
|
|
586
|
+
return undefined;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
async loadModeView() {
|
|
590
|
+
const mode = this.mode;
|
|
591
|
+
if (!mode?.kind) {
|
|
592
|
+
this.modeView = undefined;
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
const token = ++this.modeViewToken;
|
|
596
|
+
try {
|
|
597
|
+
const result = await client.query({ query: ASSISTANT_MODE_QUERY, variables: { kind: mode.kind }, fetchPolicy: 'cache-first' });
|
|
598
|
+
/* A late answer for a conversation the person has already left is dropped. */
|
|
599
|
+
if (token !== this.modeViewToken)
|
|
600
|
+
return;
|
|
601
|
+
this.modeView = result.data?.assistantMode;
|
|
602
|
+
this.rebuildMentionCaches();
|
|
603
|
+
}
|
|
604
|
+
catch (error) {
|
|
605
|
+
if (token !== this.modeViewToken)
|
|
606
|
+
return;
|
|
607
|
+
this.modeView = undefined;
|
|
608
|
+
/*
|
|
609
|
+
* A kind this server does not have is a mismatched deployment, and the person should read it
|
|
610
|
+
* rather than watch an empty `/` list.
|
|
611
|
+
*/
|
|
612
|
+
this.errorMessage = error instanceof Error ? error.message : String(error);
|
|
613
|
+
}
|
|
454
614
|
}
|
|
455
615
|
rebuildMentionCaches() {
|
|
456
616
|
this.cachedCatalogCandidates = buildCatalogCandidates(this.conversation.catalogEntries);
|
|
@@ -484,6 +644,52 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
|
|
|
484
644
|
}
|
|
485
645
|
|
|
486
646
|
/* ── C4 Roster (participants + presence) ───────────── */
|
|
647
|
+
/* 이 대화의 주어 — 같은 화면 위에 두 대화가 설 때만 나온다(ADR-0072 A17 ①).
|
|
648
|
+
지금 대화가 제목 굵기로 서고, 바꿀 자리가 그 옆에 붙는다. 세션 탭과 같은 무게로 그리지 않는다
|
|
649
|
+
(UX 레인, 2026-09-18) — 한쪽은 화제를 바꾸고 한쪽은 실타래를 바꾼다. */
|
|
650
|
+
.mode-subject {
|
|
651
|
+
display: flex;
|
|
652
|
+
align-items: baseline;
|
|
653
|
+
gap: 8px;
|
|
654
|
+
padding: 6px 12px;
|
|
655
|
+
border-bottom: 1px solid var(--md-sys-color-outline-variant, #e2e8f0);
|
|
656
|
+
}
|
|
657
|
+
.mode-now {
|
|
658
|
+
/* 고른 상태의 색은 세션 탭과 같은 계열이다 — 한 패널 안에서 「골랐다」가 두 색으로 나오면 읽는
|
|
659
|
+
사람은 두 색이 서로 다른 뜻인 줄 안다.
|
|
660
|
+
|
|
661
|
+
12px 이었다가 한 단 내렸다. 도크 제목(figure 의 .scope-title 이 12.5px / 650)과 0.5px ·
|
|
662
|
+
굵기 50 차이는 눈에 닿지 않아서, 거의 같은 제목 줄이 둘 포개졌다. 도크 제목은 그 도크 전체를
|
|
663
|
+
거느리고 이 줄은 그 안 대화 하나의 성질이다 — 11px 이면 이 패널의 다른 조작부와 같은 단이고,
|
|
664
|
+
「이것이 주어다」는 색이 계속 진다(UX 레인, 2026-09-18). */
|
|
665
|
+
color: var(--primary-color, #007f87);
|
|
666
|
+
font-size: 11px;
|
|
667
|
+
font-weight: 600;
|
|
668
|
+
}
|
|
669
|
+
.mode-switch,
|
|
670
|
+
.mode-select {
|
|
671
|
+
appearance: none;
|
|
672
|
+
background: transparent;
|
|
673
|
+
border: 1px solid var(--md-sys-color-outline-variant, #cbd5e1);
|
|
674
|
+
border-radius: 999px;
|
|
675
|
+
color: var(--md-sys-color-on-surface-variant, #475569);
|
|
676
|
+
cursor: pointer;
|
|
677
|
+
font: inherit;
|
|
678
|
+
font-size: 11px;
|
|
679
|
+
padding: 2px 10px;
|
|
680
|
+
white-space: nowrap;
|
|
681
|
+
}
|
|
682
|
+
.mode-switch:hover:not([disabled]) {
|
|
683
|
+
background: color-mix(in srgb, var(--primary-color, #007f87) 7%, transparent);
|
|
684
|
+
color: var(--primary-color, #007f87);
|
|
685
|
+
}
|
|
686
|
+
/* 답하는 동안에도 「내가 지금 무엇에 대해 말하고 있나」는 또렷해야 한다 — 흐려지는 것은 바꿀
|
|
687
|
+
자리뿐이다. 지금 대화까지 흐리면 그 한 줄이 가장 안 보인다. */
|
|
688
|
+
.mode-subject [disabled] {
|
|
689
|
+
cursor: default;
|
|
690
|
+
opacity: 0.5;
|
|
691
|
+
}
|
|
692
|
+
|
|
487
693
|
.roster {
|
|
488
694
|
display: flex;
|
|
489
695
|
align-items: center;
|
|
@@ -1034,6 +1240,29 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
|
|
|
1034
1240
|
overflow: visible;
|
|
1035
1241
|
}
|
|
1036
1242
|
|
|
1243
|
+
/* 값이 안 온 줄 — ADR-0073 판정 1.
|
|
1244
|
+
말풍선의 자리와 「AI」 딱지는 지킨다 — 그 줄은 어시스턴트의 답이 맞고, 읽는 사람이 못 지나간
|
|
1245
|
+
것이다. 점선 테두리와 흐린 색이 「이것은 그가 쓴 문장이 아니다」를 말한다.
|
|
1246
|
+
|
|
1247
|
+
기울임은 쓰지 않는다. 이 앱의 글꼴 묶음에 한글 이탤릭 자소가 없어 브라우저가 합성으로 기울이고,
|
|
1248
|
+
획 균형이 무너져 작은 글자가 먼저 안 읽힌다. UX 레인이 :4000 에서 재 보았다 — 「권한」의 진행
|
|
1249
|
+
폭이 normal·italic 둘 다 27.6875px 로 같다(2026-09-18). */
|
|
1250
|
+
.msg.redacted {
|
|
1251
|
+
background: transparent;
|
|
1252
|
+
border: 1px dashed var(--md-sys-color-outline-variant, #cbd5e1);
|
|
1253
|
+
color: var(--md-sys-color-outline, #94a3b8);
|
|
1254
|
+
}
|
|
1255
|
+
.redacted-body {
|
|
1256
|
+
align-items: center;
|
|
1257
|
+
display: flex;
|
|
1258
|
+
gap: 6px;
|
|
1259
|
+
}
|
|
1260
|
+
.redacted-body md-icon {
|
|
1261
|
+
/* md-icon 은 자기 크기를 이 변수로 잡는다 — width/height 만 주면 아무 일도 안 일어난다. */
|
|
1262
|
+
--md-icon-size: 14px;
|
|
1263
|
+
font-size: 14px;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1037
1266
|
.msg.pending {
|
|
1038
1267
|
opacity: 0.5;
|
|
1039
1268
|
}
|
|
@@ -1894,6 +2123,20 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
|
|
|
1894
2123
|
cursor: not-allowed;
|
|
1895
2124
|
}
|
|
1896
2125
|
|
|
2126
|
+
/* 보내기 전에 보이는 초대 안내 — 누르기 전에 알아야 하는 사실이다. */
|
|
2127
|
+
.invite-notice {
|
|
2128
|
+
display: flex;
|
|
2129
|
+
align-items: center;
|
|
2130
|
+
gap: 6px;
|
|
2131
|
+
margin: 0 2px 4px;
|
|
2132
|
+
color: var(--md-sys-color-on-surface-variant, #475569);
|
|
2133
|
+
font-size: 11.5px;
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
.invite-notice md-icon {
|
|
2137
|
+
--md-icon-size: 15px;
|
|
2138
|
+
}
|
|
2139
|
+
|
|
1897
2140
|
/* ── Send button — ChatGPT 스타일 원형 ↑ (.input-row 안의 button 만) ─── */
|
|
1898
2141
|
.input-row > button {
|
|
1899
2142
|
width: 32px;
|
|
@@ -1915,6 +2158,15 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
|
|
|
1915
2158
|
align-self: flex-end;
|
|
1916
2159
|
margin-bottom: 2px;
|
|
1917
2160
|
}
|
|
2161
|
+
/*
|
|
2162
|
+
방에 말하는 자리는 **두 번째 행동**이다 — 이 화면의 첫 행동은 어시스턴트에게 묻는 것이다.
|
|
2163
|
+
같은 무게로 두면 무엇이 기본인지 화면이 말하지 않게 된다(ADR-0070 A15 의 자리 둘).
|
|
2164
|
+
*/
|
|
2165
|
+
.input-row > button.say-to-room {
|
|
2166
|
+
background: var(--md-sys-color-surface-container-highest, #e2e8f0);
|
|
2167
|
+
color: var(--md-sys-color-on-surface-variant, #475569);
|
|
2168
|
+
}
|
|
2169
|
+
|
|
1918
2170
|
.input-row > button:disabled {
|
|
1919
2171
|
background: var(--md-sys-color-surface-container-highest, #e2e8f0);
|
|
1920
2172
|
color: var(--md-sys-color-outline, #94a3b8);
|
|
@@ -1949,6 +2201,25 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
|
|
|
1949
2201
|
// 첫 마운트 시 sessionId 가 이미 set 돼있어도 loadHistory 는 첫 render 후 updated()
|
|
1950
2202
|
// 가 처리 — 단일 진입점으로 일관 (이전엔 connectedCallback 과 updated 양쪽이 호출 →
|
|
1951
2203
|
// 동시 fetch race 발생).
|
|
2204
|
+
/* 모드가 이미 붙어 있으면 무엇을 주는지 먼저 묻는다 — 바뀔 때는 updated 가 다시 묻는다. */
|
|
2205
|
+
if (this.mode?.kind)
|
|
2206
|
+
this.loadModeView();
|
|
2207
|
+
if (this.subject)
|
|
2208
|
+
this.onSubjectChanged();
|
|
2209
|
+
}
|
|
2210
|
+
/**
|
|
2211
|
+
* 화면이 보여 주는 것의 종류가 바뀌었다 — 설 수 있는 대화를 다시 묻고, 지난번 고른 것을 되살린다.
|
|
2212
|
+
*
|
|
2213
|
+
* 되살리는 것은 **고른 대화**이지 세션이 아니다. 세션은 자기가 어느 대화였는지 이미 알고, 다른
|
|
2214
|
+
* 대화로 가면 그때 새로 난다(ADR-0072 A17 ①).
|
|
2215
|
+
*/
|
|
2216
|
+
async onSubjectChanged() {
|
|
2217
|
+
await this.loadModeChoices();
|
|
2218
|
+
if (this.modeChoices.length < 2 || !this.subject || !this.mode)
|
|
2219
|
+
return;
|
|
2220
|
+
const kind = pickInitialMode(this.mode.kind, this.modeChoices, this.rememberedMode(this.subject));
|
|
2221
|
+
if (kind && kind !== this.mode.kind)
|
|
2222
|
+
this.mode = { ...this.mode, kind };
|
|
1952
2223
|
}
|
|
1953
2224
|
updated(changed) {
|
|
1954
2225
|
/*
|
|
@@ -1974,8 +2245,14 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
|
|
|
1974
2245
|
* The host still owns `sessionId`; it hears `assistant-mode-changed` and decides whether
|
|
1975
2246
|
* this subject gets a stored session of its own.
|
|
1976
2247
|
*/
|
|
2248
|
+
/* 보는 것의 종류가 바뀌면 설 수 있는 대화도 달라진다 — 보드에서 현장으로 넘어간 자리. */
|
|
2249
|
+
if (changed.has('subject'))
|
|
2250
|
+
this.onSubjectChanged();
|
|
1977
2251
|
if (changed.has('mode') && isDifferentConversation(changed.get('mode'), this.mode)) {
|
|
2252
|
+
/* What this conversation offers is the new mode's; ask before drawing the panel again. */
|
|
2253
|
+
this.modeView = undefined;
|
|
1978
2254
|
this.rebuildMentionCaches();
|
|
2255
|
+
this.loadModeView();
|
|
1979
2256
|
this.lines = [];
|
|
1980
2257
|
this.pickedMentions.clear();
|
|
1981
2258
|
this.pickedUserMentions.clear();
|
|
@@ -2024,6 +2301,10 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
|
|
|
2024
2301
|
disconnectedCallback() {
|
|
2025
2302
|
this._stopPresence();
|
|
2026
2303
|
this._stopMessageSubscription();
|
|
2304
|
+
if (this.elapsedTimer) {
|
|
2305
|
+
clearInterval(this.elapsedTimer);
|
|
2306
|
+
this.elapsedTimer = undefined;
|
|
2307
|
+
}
|
|
2027
2308
|
super.disconnectedCallback();
|
|
2028
2309
|
}
|
|
2029
2310
|
// ── C4 presence/roster ─────────────────────────────────────────────────────
|
|
@@ -2160,6 +2441,7 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
|
|
|
2160
2441
|
${this.toastMessage
|
|
2161
2442
|
? html `<div class="toast show">${this.toastMessage}</div>`
|
|
2162
2443
|
: nothing}
|
|
2444
|
+
${this.renderModeSubject()}
|
|
2163
2445
|
${this.renderSessionTabs()}
|
|
2164
2446
|
${this.renderRoster()}
|
|
2165
2447
|
<div class="messages" @click=${this.onBubbleClick}>
|
|
@@ -2177,8 +2459,12 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
|
|
|
2177
2459
|
const prevSameSender = !!prev && prev.role === 'user' && prev.senderEmail === line.senderEmail;
|
|
2178
2460
|
const showSenderLabel = isOther && !prevSameSender;
|
|
2179
2461
|
const content = (line.content ?? '').trim();
|
|
2180
|
-
|
|
2181
|
-
|
|
2462
|
+
/*
|
|
2463
|
+
* 가려진 줄은 어시스턴트의 말이 아니다 — 마크다운으로 그리지 않고, 복사·재실행 같은 손잡이도
|
|
2464
|
+
* 달지 않는다(ADR-0073 판정 1). 그 손잡이들은 있지도 않은 답을 다룬다.
|
|
2465
|
+
*/
|
|
2466
|
+
const isMarkdown = line.role !== 'system' && content.length > 0 && !line.redacted;
|
|
2467
|
+
const showActions = line.role !== 'system' && !line.pending && content.length > 0 && !line.redacted;
|
|
2182
2468
|
return html `
|
|
2183
2469
|
${showLabel
|
|
2184
2470
|
? html `<div class="msg-label">${i18next.t('ai-assistant.label.ai')}</div>`
|
|
@@ -2189,19 +2475,21 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
|
|
|
2189
2475
|
<div
|
|
2190
2476
|
class="msg ${line.role} ${isOther ? 'other' : ''} ${line.pending ? 'pending' : ''} ${line.systemOpen
|
|
2191
2477
|
? 'open'
|
|
2192
|
-
: ''}">
|
|
2478
|
+
: ''} ${line.redacted ? 'redacted' : ''}">
|
|
2193
2479
|
${line.pending
|
|
2194
2480
|
? html `<span class="typing-dots"><span></span><span></span><span></span></span>`
|
|
2195
|
-
:
|
|
2196
|
-
? html `<div class="
|
|
2197
|
-
:
|
|
2198
|
-
? html `<
|
|
2481
|
+
: line.redacted
|
|
2482
|
+
? html `<div class="redacted-body"><md-icon>lock</md-icon><span>${content}</span></div>`
|
|
2483
|
+
: isMarkdown
|
|
2484
|
+
? html `<div class="md-body">${unsafeHTML(renderMarkdown(content))}</div>`
|
|
2485
|
+
: line.role === 'system'
|
|
2486
|
+
? html `<span
|
|
2199
2487
|
class="sys-text"
|
|
2200
2488
|
title=${i18next.t('ai-assistant.text.toggle-system-note', { defaultValue: 'click to expand' })}
|
|
2201
2489
|
@click=${() => this.toggleSystemNote(idx)}
|
|
2202
2490
|
>${content}</span
|
|
2203
2491
|
>`
|
|
2204
|
-
|
|
2492
|
+
: content}
|
|
2205
2493
|
${line.attachments && line.attachments.length > 0
|
|
2206
2494
|
? html `
|
|
2207
2495
|
<div class="msg-attachments">
|
|
@@ -2348,6 +2636,7 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
|
|
|
2348
2636
|
</div>
|
|
2349
2637
|
`
|
|
2350
2638
|
: nothing}
|
|
2639
|
+
${this.renderInviteNotice()}
|
|
2351
2640
|
<div class="input-row">
|
|
2352
2641
|
<div class="textarea-wrap">
|
|
2353
2642
|
<textarea
|
|
@@ -2361,7 +2650,9 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
|
|
|
2361
2650
|
}}
|
|
2362
2651
|
?disabled=${this.busy}
|
|
2363
2652
|
rows="1"
|
|
2364
|
-
placeholder=${this.placeholder ??
|
|
2653
|
+
placeholder=${this.conversation.placeholder ??
|
|
2654
|
+
this.placeholder ??
|
|
2655
|
+
i18next.t('ai-assistant.text.input-placeholder')}></textarea>
|
|
2365
2656
|
${this.mentionOpen
|
|
2366
2657
|
? html `
|
|
2367
2658
|
<ox-mention-popup
|
|
@@ -2381,6 +2672,27 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
|
|
|
2381
2672
|
`
|
|
2382
2673
|
: nothing}
|
|
2383
2674
|
</div>
|
|
2675
|
+
${ /*
|
|
2676
|
+
두 자리다 — 어시스턴트에게 묻는 자리와 **방에 말하는 자리**. 누른 자리가 그 말의 대상을
|
|
2677
|
+
정하고, 서버는 어느 문이 불렸는지로 그 사실을 저장한다(ADR-0070 A15 결정 ①).
|
|
2678
|
+
|
|
2679
|
+
**방에 사람이 있을 때만 보인다.** 혼자 쓰는 대화에서 「방에 말하기」는 아무에게도 하지 않는
|
|
2680
|
+
말이고, 누를 일이 없는 단추는 무엇이 기본인지 흐린다. 조건은 참여자 목록과 같다(둘 이상) —
|
|
2681
|
+
둘째 사람은 멘션으로 들어오고, 그 순간 이 자리도 함께 선다.
|
|
2682
|
+
*/''}
|
|
2683
|
+
${this.sessionId && this.participants.length >= 2
|
|
2684
|
+
? html `
|
|
2685
|
+
<button
|
|
2686
|
+
class="say-to-room"
|
|
2687
|
+
?disabled=${this.busy || !this.input.trim()}
|
|
2688
|
+
@click=${() => this.sayToRoom()}
|
|
2689
|
+
title=${i18next.t('ai-assistant.text.say-to-room-tooltip', {
|
|
2690
|
+
defaultValue: 'say this to the people here — the assistant does not answer'
|
|
2691
|
+
})}>
|
|
2692
|
+
<md-icon>forum</md-icon>
|
|
2693
|
+
</button>
|
|
2694
|
+
`
|
|
2695
|
+
: nothing}
|
|
2384
2696
|
<button
|
|
2385
2697
|
?disabled=${this.busy || (!this.input.trim() && this.attachments.length === 0)}
|
|
2386
2698
|
@click=${this.send}
|
|
@@ -2393,6 +2705,82 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
|
|
|
2393
2705
|
</div>
|
|
2394
2706
|
`;
|
|
2395
2707
|
}
|
|
2708
|
+
/**
|
|
2709
|
+
* 방에 말한다 — 어시스턴트는 답하지 않는다.
|
|
2710
|
+
*
|
|
2711
|
+
* 저장되는 대화가 있을 때만 할 수 있다. 세션이 없는 즉석 질문에는 「방」이 없다 — 그 자리에서
|
|
2712
|
+
* 한 말은 누구에게도 남지 않는다.
|
|
2713
|
+
*/
|
|
2714
|
+
async sayToRoom() {
|
|
2715
|
+
const body = this.input.trim();
|
|
2716
|
+
if (!body || this.busy)
|
|
2717
|
+
return;
|
|
2718
|
+
if (!this.sessionId) {
|
|
2719
|
+
/* 말할 방이 없다는 사실을 말한다 — 조용히 버리면 사람은 말했다고 믿는다. */
|
|
2720
|
+
this.errorMessage = i18next.t('ai-assistant.text.no-room-yet', {
|
|
2721
|
+
defaultValue: 'this conversation is not saved yet, so there is nobody else in it to talk to'
|
|
2722
|
+
});
|
|
2723
|
+
return;
|
|
2724
|
+
}
|
|
2725
|
+
const localId = `local-${++this._localSeq}-r`;
|
|
2726
|
+
this.lines = [...this.lines, { role: 'user', content: body, _localId: localId, senderEmail: this._myEmail }];
|
|
2727
|
+
this.input = '';
|
|
2728
|
+
this.busy = true;
|
|
2729
|
+
try {
|
|
2730
|
+
const result = await client.mutate({
|
|
2731
|
+
mutation: SAY_TO_ROOM_MUTATION,
|
|
2732
|
+
variables: { sessionId: this.sessionId, content: body }
|
|
2733
|
+
});
|
|
2734
|
+
/* 방송 에코와 겹치지 않게 서버 id 로 맞춘다 — 보낸 말이 두 줄로 보이지 않게. */
|
|
2735
|
+
this._reconcileSentLine(localId, result.data?.sayToRoom ?? undefined, {});
|
|
2736
|
+
}
|
|
2737
|
+
catch (error) {
|
|
2738
|
+
this.lines = this.lines.filter(line => line._localId !== localId);
|
|
2739
|
+
this.input = body;
|
|
2740
|
+
this.errorMessage = error instanceof Error ? error.message : String(error);
|
|
2741
|
+
}
|
|
2742
|
+
finally {
|
|
2743
|
+
this.busy = false;
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2746
|
+
/**
|
|
2747
|
+
* 보내기 전에 말한다 — 「이 사람은 이 대화에 들어옵니다」(ADR-0071 결정 1 ④).
|
|
2748
|
+
*
|
|
2749
|
+
* 멘션은 사람을 불러들인다. 누른 뒤에 알면 이미 들어와 있고, 협의의 공개 범위는 되돌리기 어려운
|
|
2750
|
+
* 종류의 일이다. 그래서 **아직 보내지 않은 상태에서** 누가 들어오게 되는지 보인다.
|
|
2751
|
+
*
|
|
2752
|
+
* 초대할 수 없는 자리(보기 권한 · 아직 참여자가 아님)라면 그 사실을 말한다 — 눌렀는데 아무 일도
|
|
2753
|
+
* 일어나지 않는 것이 가장 나쁘다.
|
|
2754
|
+
*/
|
|
2755
|
+
renderInviteNotice() {
|
|
2756
|
+
if (!this.sessionId)
|
|
2757
|
+
return nothing;
|
|
2758
|
+
const here = new Set(this.participants.map(p => p.user?.id).filter(Boolean));
|
|
2759
|
+
const incoming = [...this.pickedUserMentions.entries()]
|
|
2760
|
+
.filter(([token, userId]) => this.input.includes(`@${token}`) && !here.has(userId))
|
|
2761
|
+
.map(([token]) => token);
|
|
2762
|
+
if (incoming.length === 0)
|
|
2763
|
+
return nothing;
|
|
2764
|
+
const myRole = this.participants.find(p => p.user?.id && p.user.id === this._myUserId)?.role;
|
|
2765
|
+
const mayInvite = myRole === 'owner' || myRole === 'member';
|
|
2766
|
+
const names = incoming.map(token => `@${token}`).join(', ');
|
|
2767
|
+
return html `
|
|
2768
|
+
<div class="invite-notice">
|
|
2769
|
+
<md-icon>person_add</md-icon>
|
|
2770
|
+
<span>
|
|
2771
|
+
${mayInvite
|
|
2772
|
+
? i18next.t('ai-assistant.text.will-be-invited', {
|
|
2773
|
+
names,
|
|
2774
|
+
defaultValue: '{names} will join this conversation when you send'
|
|
2775
|
+
})
|
|
2776
|
+
: i18next.t('ai-assistant.text.cannot-invite', {
|
|
2777
|
+
names,
|
|
2778
|
+
defaultValue: '{names} will not join — only the people already in this conversation can bring someone in'
|
|
2779
|
+
})}
|
|
2780
|
+
</span>
|
|
2781
|
+
</div>
|
|
2782
|
+
`;
|
|
2783
|
+
}
|
|
2396
2784
|
onKeyDown(e) {
|
|
2397
2785
|
// Mention popup 활성 시 — 화살표/Enter/Tab/Esc 가로채서 popup 네비게이션
|
|
2398
2786
|
if (this.mentionOpen) {
|
|
@@ -2512,14 +2900,32 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
|
|
|
2512
2900
|
// 매칭된 후보 outline 만 갱신 — 카메라/선택 변경은 commit (Enter/click) 시점에만.
|
|
2513
2901
|
this.highlightMatchedCandidates();
|
|
2514
2902
|
}
|
|
2903
|
+
/**
|
|
2904
|
+
* 호스트가 목록을 주지 않을 때 이 도메인의 사용자를 직접 묻는다.
|
|
2905
|
+
*
|
|
2906
|
+
* 실패는 빈 목록이다 — 멘션을 못 고르는 것이지 대화가 막힐 일은 아니다.
|
|
2907
|
+
*/
|
|
2908
|
+
async fetchDomainUsers(query) {
|
|
2909
|
+
try {
|
|
2910
|
+
const result = await client.query({
|
|
2911
|
+
query: DOMAIN_USERS_FOR_MENTION_QUERY,
|
|
2912
|
+
variables: { query },
|
|
2913
|
+
fetchPolicy: 'network-only'
|
|
2914
|
+
});
|
|
2915
|
+
return result.data?.domainUsersForMention ?? [];
|
|
2916
|
+
}
|
|
2917
|
+
catch {
|
|
2918
|
+
return [];
|
|
2919
|
+
}
|
|
2920
|
+
}
|
|
2515
2921
|
/** `@` query 에 대한 userProvider 비동기 호출 — 응답 도착 시 popup 결과 갱신.
|
|
2516
2922
|
* race 방지: token 으로 최신 호출 식별 (늦게 도착한 응답 무시). */
|
|
2517
2923
|
async refreshUserCandidatesAsync(query, hashPos) {
|
|
2518
|
-
if (!this.userProvider)
|
|
2519
|
-
return;
|
|
2520
2924
|
const token = ++this.userFetchToken;
|
|
2521
2925
|
try {
|
|
2522
|
-
const users =
|
|
2926
|
+
const users = this.userProvider
|
|
2927
|
+
? await Promise.resolve(this.userProvider(query))
|
|
2928
|
+
: await this.fetchDomainUsers(query);
|
|
2523
2929
|
if (token !== this.userFetchToken)
|
|
2524
2930
|
return; // 더 최신 호출이 있음
|
|
2525
2931
|
// popup 이 닫혔거나 trigger / hashPos 가 바뀌면 무시
|
|
@@ -2695,6 +3101,59 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
|
|
|
2695
3101
|
]; }
|
|
2696
3102
|
/** 보드의 세션 탭 — 다중 세션 활성화 시 패널 상단. host 가 sessions 비워두면 표시 X.
|
|
2697
3103
|
* 레이블은 session.name 우선, 없으면 fallback "세션 N". */
|
|
3104
|
+
/**
|
|
3105
|
+
* 이 대화의 **주어** — 같은 화면 위에 두 대화가 설 때만 나온다(ADR-0072 A17 ①).
|
|
3106
|
+
*
|
|
3107
|
+
* 하나뿐이면 아무것도 그리지 않는다. 고를 것이 없는 고르개는 자리만 차지하고, 사람에게 「무언가
|
|
3108
|
+
* 고를 것이 있다」는 잘못된 신호를 준다.
|
|
3109
|
+
*
|
|
3110
|
+
* ── 왜 탭 줄이 아닌가 (UX 레인, 2026-09-18) ───────────────────────────────
|
|
3111
|
+
* 처음에는 칩 줄에 `role="tablist"` 를 달았다. 바로 아래가 세션 탭이고 그쪽도 tablist 라서, 화면
|
|
3112
|
+
* 낭독기로 들으면 「탭 목록, 대화」 다음에 「탭 목록, Conversation history」 가 이어졌다 — 고르는
|
|
3113
|
+
* 축이 둘인데 둘 다 「탭」이라고 말한다. 게다가 ARIA 탭은 좌우 화살표 이동과 `tabpanel` 이 함께
|
|
3114
|
+
* 있어야 성립하고, 둘 다 없으면 낭독기는 「2 중 1번째 탭」이라 말하고 화살표는 아무 일도 안 한다.
|
|
3115
|
+
*
|
|
3116
|
+
* 무게도 다르다. 세션은 「어느 지난 대화냐」이고 모드는 「이 대화가 무엇에 대한 것이냐」이다. 같은
|
|
3117
|
+
* 무게로 그리면 어느 쪽이 화제를 바꾸고 어느 쪽이 실타래를 바꾸는지 구별되지 않는다. 그래서 지금
|
|
3118
|
+
* 대화가 제목 굵기로 서고, 바꿀 자리는 그 옆에 붙는다.
|
|
3119
|
+
*
|
|
3120
|
+
* ── 왜 도크 제목 줄로 안 올리나 (UX 레인 판정, 2026-09-18) ────────────────
|
|
3121
|
+
* 도크 제목은 「지금 어디에 있나」(이 도형 · 이 현장 · 이 스테이션 화면)이고 이 줄은 「이 대화가
|
|
3122
|
+
* 무엇에 대한 것이냐」이다. 대화의 주어는 대화 안에 있다. 그리고 세 도크가 각자 그리면 세 벌이 되고
|
|
3123
|
+
* 그 세 벌은 갈린다 — `modeChoices` 를 아는 것도 여기다. 올리면 목록과 고른 것이 세 자리로 나갔다
|
|
3124
|
+
* 돌아온다.
|
|
3125
|
+
*/
|
|
3126
|
+
renderModeSubject() {
|
|
3127
|
+
if (this.modeChoices.length < 2)
|
|
3128
|
+
return nothing;
|
|
3129
|
+
const current = this.modeChoices.find(choice => choice.kind === this.mode?.kind);
|
|
3130
|
+
const others = this.modeChoices.filter(choice => choice.kind !== this.mode?.kind);
|
|
3131
|
+
return html `
|
|
3132
|
+
<div class="mode-subject">
|
|
3133
|
+
<span class="mode-now">${current?.label ?? this.mode?.kind ?? ''}</span>
|
|
3134
|
+
${others.length === 1
|
|
3135
|
+
? html `
|
|
3136
|
+
<button
|
|
3137
|
+
class="mode-switch"
|
|
3138
|
+
title=${i18next.t('ai-assistant.text.switch-conversation', { name: others[0].label })}
|
|
3139
|
+
aria-label=${i18next.t('ai-assistant.text.switch-conversation', { name: others[0].label })}
|
|
3140
|
+
?disabled=${this.busy}
|
|
3141
|
+
@click=${() => this.onModePick(others[0].kind)}>
|
|
3142
|
+
${others[0].label}
|
|
3143
|
+
</button>
|
|
3144
|
+
`
|
|
3145
|
+
: html `
|
|
3146
|
+
<select
|
|
3147
|
+
class="mode-select"
|
|
3148
|
+
aria-label=${i18next.t('ai-assistant.label.conversation')}
|
|
3149
|
+
?disabled=${this.busy}
|
|
3150
|
+
@change=${(event) => this.onModePick(event.target.value)}>
|
|
3151
|
+
${this.modeChoices.map(choice => html `<option value=${choice.kind} ?selected=${choice.kind === this.mode?.kind}>${choice.label}</option>`)}
|
|
3152
|
+
</select>
|
|
3153
|
+
`}
|
|
3154
|
+
</div>
|
|
3155
|
+
`;
|
|
3156
|
+
}
|
|
2698
3157
|
renderSessionTabs() {
|
|
2699
3158
|
if (!this.sessions || this.sessions.length === 0)
|
|
2700
3159
|
return nothing;
|
|
@@ -2780,8 +3239,8 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
|
|
|
2780
3239
|
<span class="badge">${i18next.t('ai-assistant.label.korean-supported')}</span>
|
|
2781
3240
|
<span class="badge">${i18next.t('ai-assistant.label.multi-command')}</span>
|
|
2782
3241
|
<span class="badge">${i18next.t('ai-assistant.label.review-able')}</span>
|
|
2783
|
-
${this.conversation.
|
|
2784
|
-
? html `<br />${
|
|
3242
|
+
${this.conversation.footerNotice
|
|
3243
|
+
? html `<br />${this.conversation.footerNotice}`
|
|
2785
3244
|
: ''}
|
|
2786
3245
|
</div>
|
|
2787
3246
|
</div>
|
|
@@ -2942,7 +3401,8 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
|
|
|
2942
3401
|
class="run"
|
|
2943
3402
|
?disabled=${sent}
|
|
2944
3403
|
@click=${() => this.executeProposal(p, key)}>
|
|
2945
|
-
${sent
|
|
3404
|
+
${sent
|
|
3405
|
+
? this.proposalStateText(key)
|
|
2946
3406
|
: i18next.t('ai-assistant.button.run-proposal', { defaultValue: 'run' })}
|
|
2947
3407
|
</button>
|
|
2948
3408
|
${ /*
|
|
@@ -2951,15 +3411,15 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
|
|
|
2951
3411
|
This badge used to read "there are chips" as "the model is in the 3D viewer" and
|
|
2952
3412
|
say so, in a component that plant and twin also use.
|
|
2953
3413
|
*/''}
|
|
2954
|
-
${this.stagedProposals.has(key) && this.conversation.
|
|
3414
|
+
${this.stagedProposals.has(key) && this.conversation.stagedNotice ? html `
|
|
2955
3415
|
<span class="staged-badge">
|
|
2956
3416
|
<md-icon>check_circle</md-icon>
|
|
2957
|
-
<span>${
|
|
3417
|
+
<span>${this.conversation.stagedNotice}</span>
|
|
2958
3418
|
</span>
|
|
2959
3419
|
` : nothing}
|
|
2960
3420
|
</div>
|
|
2961
3421
|
${rawChoices.length ? html `
|
|
2962
|
-
<div class="proposal-choices" role="group" aria-label
|
|
3422
|
+
<div class="proposal-choices" role="group" aria-label=${i18next.t('ai-assistant.text.proposal-choices', { defaultValue: 'follow-up choices' })}>
|
|
2963
3423
|
${rawChoices.map((c) => html `
|
|
2964
3424
|
<button
|
|
2965
3425
|
class="choice-chip ${c.primary ? 'primary' : ''}"
|
|
@@ -2976,6 +3436,43 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
|
|
|
2976
3436
|
</div>
|
|
2977
3437
|
`;
|
|
2978
3438
|
}
|
|
3439
|
+
/**
|
|
3440
|
+
* 눌린 카드가 말하는 것 — **호스트가 한 일**이고, 아직 모르면 모른다고 말한다.
|
|
3441
|
+
*
|
|
3442
|
+
* 예전에는 누른 순간 「실행됨」이었다. 09-18 트윈에서 아무 일도 일어나지 않은 채 「✓ 반영했습니다」가
|
|
3443
|
+
* 떴다(ADR-0070 A12-a).
|
|
3444
|
+
*/
|
|
3445
|
+
proposalStateText(key) {
|
|
3446
|
+
const outcome = this.proposalOutcomes.get(key);
|
|
3447
|
+
if (!outcome) {
|
|
3448
|
+
/* 답이 오기 전 — 아는 사실은 「보냈고 아직 답이 없다」뿐이다. 경과 시간을 함께 보인다. */
|
|
3449
|
+
const since = this.proposalSentAt.get(key);
|
|
3450
|
+
const seconds = since ? Math.max(0, Math.round((Date.now() - since) / 1000)) : 0;
|
|
3451
|
+
return i18next.t('ai-assistant.text.proposal-waiting', {
|
|
3452
|
+
seconds,
|
|
3453
|
+
defaultValue: 'sending — {seconds}s, no answer yet'
|
|
3454
|
+
});
|
|
3455
|
+
}
|
|
3456
|
+
const counted = typeof outcome.applied === 'number' && typeof outcome.total === 'number' && outcome.total > 0
|
|
3457
|
+
? i18next.t('ai-assistant.text.proposal-applied-count', {
|
|
3458
|
+
applied: outcome.applied,
|
|
3459
|
+
total: outcome.total,
|
|
3460
|
+
defaultValue: ' {applied} of {total}'
|
|
3461
|
+
})
|
|
3462
|
+
: '';
|
|
3463
|
+
/* 사유는 호스트가 준 코드 그대로다 — 문장으로 바꾸지 않는다(ADR-0042). */
|
|
3464
|
+
const reason = outcome.errorCode ? ` (${outcome.errorCode})` : '';
|
|
3465
|
+
const verdict = i18next.t(`ai-assistant.verdict.${outcome.verdict}`, {
|
|
3466
|
+
defaultValue: outcome.verdict === 'accepted'
|
|
3467
|
+
? 'done'
|
|
3468
|
+
: outcome.verdict === 'refused'
|
|
3469
|
+
? 'refused'
|
|
3470
|
+
: outcome.verdict === 'declined'
|
|
3471
|
+
? 'cancelled'
|
|
3472
|
+
: 'could not run'
|
|
3473
|
+
});
|
|
3474
|
+
return `${verdict}${counted}${reason}`;
|
|
3475
|
+
}
|
|
2979
3476
|
onChoiceClick(choice, proposal, key) {
|
|
2980
3477
|
if (choice.action === 'save') {
|
|
2981
3478
|
this.dispatchEvent(new CustomEvent('assistant-choice', {
|
|
@@ -3020,25 +3517,67 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
|
|
|
3020
3517
|
.map(k => `${JSON.stringify(k)}:${stable(v[k])}`)
|
|
3021
3518
|
.join(',')}}`;
|
|
3022
3519
|
};
|
|
3520
|
+
/*
|
|
3521
|
+
* 서버가 실어 보낸 키가 정본이다 — 무엇이 같은 조치인가는 제안을 만든 쪽이 안다(ADR-0061 결정
|
|
3522
|
+
* 7 뒤 정리). 예전에는 여기서 `command|instanceId|args` 로 다시 셌는데, 명령이 없는 제안(선언 ·
|
|
3523
|
+
* 새 트윈 초안)은 그 셋이 모두 비어 **서로 다른 제안이 한 카드로 접혔고**, 하나를 누르면 나머지가
|
|
3524
|
+
* 함께 「요청됨」이 됐다.
|
|
3525
|
+
*/
|
|
3526
|
+
if (typeof p?.key === 'string' && p.key)
|
|
3527
|
+
return p.key;
|
|
3023
3528
|
try {
|
|
3024
|
-
return `${p?.command ?? ''}|${p?.instanceId ?? ''}|${stable(p?.args)}`;
|
|
3529
|
+
return `${p?.tool ?? ''}|${p?.command ?? ''}|${p?.instanceId ?? ''}|${stable(p?.args)}|${stable(p?.ops)}`;
|
|
3025
3530
|
}
|
|
3026
3531
|
catch {
|
|
3027
|
-
return `${p?.command ?? ''}|${p?.instanceId ?? ''}`;
|
|
3532
|
+
return `${p?.tool ?? ''}|${p?.command ?? ''}|${p?.instanceId ?? ''}`;
|
|
3028
3533
|
}
|
|
3029
3534
|
}
|
|
3030
|
-
/**
|
|
3031
|
-
* 실행은 호스트가 한다 — 이 컴포넌트는 도메인(트윈 명령 채널)을 모른다.
|
|
3032
|
-
* 확인 대화·명령 전송·결과 기록은 호스트의 몫이고, 여기서는 중복 클릭만 막는다.
|
|
3033
|
-
*/
|
|
3034
3535
|
executeProposal(p, key) {
|
|
3035
3536
|
this.sentProposals = new Set(this.sentProposals).add(key);
|
|
3537
|
+
this.proposalSentAt.set(key, Date.now());
|
|
3538
|
+
this._startElapsedTicker();
|
|
3036
3539
|
this.dispatchEvent(new CustomEvent('assistant-proposal-execute', {
|
|
3037
|
-
|
|
3540
|
+
/*
|
|
3541
|
+
* `proposalKey` 를 함께 준다 — 호스트가 `reportProposalOutcome` 으로 답할 때 쓰는 이름이다.
|
|
3542
|
+
* 호스트가 자기 규칙으로 키를 다시 만들면 카드가 못 찾는 답이 생긴다(원본은 하나).
|
|
3543
|
+
*/
|
|
3544
|
+
detail: { proposal: p, sessionId: this.sessionId, proposalKey: key },
|
|
3038
3545
|
bubbles: true,
|
|
3039
3546
|
composed: true
|
|
3040
3547
|
}));
|
|
3041
3548
|
}
|
|
3549
|
+
/**
|
|
3550
|
+
* 호스트가 무엇을 했는지 — 대화에 남은 `system` 행에서 읽는다(ADR-0070 A12-a 결정 ①).
|
|
3551
|
+
*
|
|
3552
|
+
* 카드가 자기 기억으로 상태를 들고 있으면 새로 고치는 순간 사라지고, 다른 창에서 실행한 것은
|
|
3553
|
+
* 애초에 보이지 않는다. 그래서 상태의 자리는 이력이다.
|
|
3554
|
+
*/
|
|
3555
|
+
get proposalOutcomes() {
|
|
3556
|
+
const outcomes = new Map();
|
|
3557
|
+
for (const line of this.lines) {
|
|
3558
|
+
if (line.role !== 'system')
|
|
3559
|
+
continue;
|
|
3560
|
+
const parsed = parseProposalOutcomeLine(line.content ?? '');
|
|
3561
|
+
if (parsed)
|
|
3562
|
+
outcomes.set(parsed.key, parsed);
|
|
3563
|
+
}
|
|
3564
|
+
return outcomes;
|
|
3565
|
+
}
|
|
3566
|
+
/** 경과 시간을 보이는 동안에만 돈다 — 답이 오면 멈춘다. */
|
|
3567
|
+
_startElapsedTicker() {
|
|
3568
|
+
if (this.elapsedTimer)
|
|
3569
|
+
return;
|
|
3570
|
+
this.elapsedTimer = setInterval(() => {
|
|
3571
|
+
const outcomes = this.proposalOutcomes;
|
|
3572
|
+
const waiting = [...this.proposalSentAt.keys()].some(key => !outcomes.has(key));
|
|
3573
|
+
if (!waiting) {
|
|
3574
|
+
clearInterval(this.elapsedTimer);
|
|
3575
|
+
this.elapsedTimer = undefined;
|
|
3576
|
+
return;
|
|
3577
|
+
}
|
|
3578
|
+
this.requestUpdate();
|
|
3579
|
+
}, 1000);
|
|
3580
|
+
}
|
|
3042
3581
|
/**
|
|
3043
3582
|
* 접지 경고 — 답이 언급했지만 **근거에 없던** 대상을 알린다.
|
|
3044
3583
|
*
|
|
@@ -3165,12 +3704,12 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
|
|
|
3165
3704
|
/** 배지 문구 — 코드가 아니라 사람 말로. */
|
|
3166
3705
|
outcomeLabel(outcome) {
|
|
3167
3706
|
const map = {
|
|
3168
|
-
ok: ['ai-assistant.text.outcome-ok', '
|
|
3169
|
-
rejected: ['ai-assistant.text.outcome-rejected', '
|
|
3170
|
-
queued: ['ai-assistant.text.outcome-queued', '
|
|
3171
|
-
proposed: ['ai-assistant.text.outcome-proposed', '
|
|
3172
|
-
folded: ['ai-assistant.text.outcome-folded', '
|
|
3173
|
-
error: ['ai-assistant.text.outcome-error', '
|
|
3707
|
+
ok: ['ai-assistant.text.outcome-ok', 'read'],
|
|
3708
|
+
rejected: ['ai-assistant.text.outcome-rejected', 'rejected'],
|
|
3709
|
+
queued: ['ai-assistant.text.outcome-queued', 'to apply'],
|
|
3710
|
+
proposed: ['ai-assistant.text.outcome-proposed', 'proposed'],
|
|
3711
|
+
folded: ['ai-assistant.text.outcome-folded', 'duplicate'],
|
|
3712
|
+
error: ['ai-assistant.text.outcome-error', 'error']
|
|
3174
3713
|
};
|
|
3175
3714
|
const [key, fallback] = map[outcome] ?? map.ok;
|
|
3176
3715
|
return i18next.t(key, { defaultValue: fallback });
|
|
@@ -3304,8 +3843,8 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
|
|
|
3304
3843
|
this.attachments = [];
|
|
3305
3844
|
const promptText = text || (attachmentsToSend.length > 0
|
|
3306
3845
|
? (attachmentsToSend[0].type.startsWith('image/')
|
|
3307
|
-
? '
|
|
3308
|
-
: '
|
|
3846
|
+
? i18next.t('ai-assistant.text.attached-image-ask', { defaultValue: 'here is an image — go by it.' })
|
|
3847
|
+
: i18next.t('ai-assistant.text.attached-file-ask', { defaultValue: 'here is a file — take a look.' }))
|
|
3309
3848
|
: '');
|
|
3310
3849
|
if (!this.sessionId && this.prepareSession) {
|
|
3311
3850
|
this.busy = true;
|
|
@@ -3325,7 +3864,7 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
|
|
|
3325
3864
|
let requestBase;
|
|
3326
3865
|
try {
|
|
3327
3866
|
const provided = this.contextProvider?.() || {};
|
|
3328
|
-
requestContext = structuredClone({
|
|
3867
|
+
requestContext = structuredClone({ hostContext: this.conversation.hostContext, ...provided });
|
|
3329
3868
|
liveBoard = structuredClone(this.boardProvider ? this.boardProvider() : this.currentBoard);
|
|
3330
3869
|
requestBase = { version: 1, sessionId: this.sessionId, hostContext: requestContext.hostContext,
|
|
3331
3870
|
modelFingerprint: assistantModelFingerprint(liveBoard) };
|
|
@@ -3407,11 +3946,9 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
|
|
|
3407
3946
|
? buildAssistantChatInput({
|
|
3408
3947
|
sessionId: this.sessionId,
|
|
3409
3948
|
history,
|
|
3410
|
-
|
|
3949
|
+
mode: { kind: this.conversation.kind, target: this.conversation.target },
|
|
3411
3950
|
truncateAfterMessageId: this._truncateAfterId,
|
|
3412
|
-
hostContext: requestContext.hostContext
|
|
3413
|
-
toolCategories: this.conversation.toolCategories,
|
|
3414
|
-
requireGroundingTools: this.requireGroundingTools
|
|
3951
|
+
hostContext: requestContext.hostContext
|
|
3415
3952
|
})
|
|
3416
3953
|
: buildChatMutationInput({
|
|
3417
3954
|
sessionId: this.sessionId,
|
|
@@ -3420,7 +3957,8 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
|
|
|
3420
3957
|
scopes: this.scopes,
|
|
3421
3958
|
truncateAfterMessageId: this._truncateAfterId,
|
|
3422
3959
|
hostContext: requestContext.hostContext,
|
|
3423
|
-
|
|
3960
|
+
/* The board door still takes its categories from the element — it declares no mode. */
|
|
3961
|
+
toolCategories: this.toolCategories,
|
|
3424
3962
|
boardTools: this.boardTools,
|
|
3425
3963
|
requireGroundingTools: this.requireGroundingTools,
|
|
3426
3964
|
knownTypes: this.knownTypes,
|
|
@@ -3560,9 +4098,9 @@ __decorate([
|
|
|
3560
4098
|
__metadata("design:type", Object)
|
|
3561
4099
|
], OxAssistantChat.prototype, "mode", void 0);
|
|
3562
4100
|
__decorate([
|
|
3563
|
-
property({ type: String
|
|
4101
|
+
property({ type: String }),
|
|
3564
4102
|
__metadata("design:type", String)
|
|
3565
|
-
], OxAssistantChat.prototype, "
|
|
4103
|
+
], OxAssistantChat.prototype, "subject", void 0);
|
|
3566
4104
|
__decorate([
|
|
3567
4105
|
property({ type: Boolean, attribute: 'require-grounding-tools' }),
|
|
3568
4106
|
__metadata("design:type", Object)
|
|
@@ -3585,7 +4123,7 @@ __decorate([
|
|
|
3585
4123
|
], OxAssistantChat.prototype, "componentSchemas", void 0);
|
|
3586
4124
|
__decorate([
|
|
3587
4125
|
property({ type: String }),
|
|
3588
|
-
__metadata("design:type",
|
|
4126
|
+
__metadata("design:type", String)
|
|
3589
4127
|
], OxAssistantChat.prototype, "placeholder", void 0);
|
|
3590
4128
|
__decorate([
|
|
3591
4129
|
state(),
|
|
@@ -3695,6 +4233,14 @@ __decorate([
|
|
|
3695
4233
|
state(),
|
|
3696
4234
|
__metadata("design:type", Object)
|
|
3697
4235
|
], OxAssistantChat.prototype, "mentionOverflow", void 0);
|
|
4236
|
+
__decorate([
|
|
4237
|
+
state(),
|
|
4238
|
+
__metadata("design:type", Object)
|
|
4239
|
+
], OxAssistantChat.prototype, "modeView", void 0);
|
|
4240
|
+
__decorate([
|
|
4241
|
+
state(),
|
|
4242
|
+
__metadata("design:type", Array)
|
|
4243
|
+
], OxAssistantChat.prototype, "modeChoices", void 0);
|
|
3698
4244
|
OxAssistantChat = OxAssistantChat_1 = __decorate([
|
|
3699
4245
|
customElement('ox-assistant-chat')
|
|
3700
4246
|
], OxAssistantChat);
|