@things-factory/ai-assistant 10.1.27 → 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
|
@@ -59,7 +59,8 @@ import {
|
|
|
59
59
|
* 뜨지 않는 것이 맞다.
|
|
60
60
|
*/
|
|
61
61
|
import { chatDefaults } from './chat-defaults.js'
|
|
62
|
-
import { isDifferentConversation, resolveConversation, type AssistantMode } from './assistant-mode.js'
|
|
62
|
+
import { isDifferentConversation, resolveConversation, type AssistantMode, type AssistantModeView, pickInitialMode } from './assistant-mode.js'
|
|
63
|
+
import { parseProposalOutcomeLine, type ParsedProposalOutcome } from './proposal-outcome-line.js'
|
|
63
64
|
import { availableTriggers, type TriggerChip } from './chat-triggers.js'
|
|
64
65
|
|
|
65
66
|
interface ChatLine {
|
|
@@ -80,6 +81,13 @@ interface ChatLine {
|
|
|
80
81
|
followUp?: string
|
|
81
82
|
patchId?: string
|
|
82
83
|
pending?: boolean
|
|
84
|
+
/**
|
|
85
|
+
* 서버가 이 사람에게 값을 안 내준 줄 — ADR-0073 판정 1.
|
|
86
|
+
*
|
|
87
|
+
* `content` 에 온 것은 「이 답은 <문> 권한이 필요합니다」 한 줄이다. 줄을 지우지 않는 이유는, 방에
|
|
88
|
+
* 무슨 일이 있었는지는 알 수 있어야 하기 때문이다. 값만 안 보인다.
|
|
89
|
+
*/
|
|
90
|
+
redacted?: boolean
|
|
83
91
|
/** AI 가 응답을 만드는 동안 호출한 도구 trace — fold-able 박스에서 노출. */
|
|
84
92
|
toolUsages?: Array<{
|
|
85
93
|
name: string
|
|
@@ -172,10 +180,80 @@ const ASSISTANT_CHAT_MUTATION = gql`
|
|
|
172
180
|
offeredTools
|
|
173
181
|
proposals
|
|
174
182
|
groundingWarnings
|
|
183
|
+
historyFolded
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
`
|
|
187
|
+
|
|
188
|
+
/*
|
|
189
|
+
* What this conversation offers on screen, in this person's language.
|
|
190
|
+
*
|
|
191
|
+
* The `/` sentences, the `@` catalog and the two notices are declared with the mode on the server
|
|
192
|
+
* (ADR-0061 decision 7) and translated there, so the panel and the door cannot describe different
|
|
193
|
+
* conversations, and a host does not carry a second copy of the words.
|
|
194
|
+
*/
|
|
195
|
+
/*
|
|
196
|
+
* 이 화면에 설 수 있는 대화들 — ADR-0072 A17 ①.
|
|
197
|
+
*
|
|
198
|
+
* 화면은 자기가 무엇을 보여 주는지만 말하고, 그 위에 어느 대화가 서는지는 앱 서버가 답한다.
|
|
199
|
+
*/
|
|
200
|
+
const ASSISTANT_MODES_QUERY = gql`
|
|
201
|
+
query AssistantModes($subject: String!) {
|
|
202
|
+
assistantModes(subject: $subject) {
|
|
203
|
+
kind
|
|
204
|
+
label
|
|
205
|
+
isDefault
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
`
|
|
209
|
+
|
|
210
|
+
const ASSISTANT_MODE_QUERY = gql`
|
|
211
|
+
query AssistantMode($kind: String!) {
|
|
212
|
+
assistantMode(kind: $kind) {
|
|
213
|
+
kind
|
|
214
|
+
slashTemplates {
|
|
215
|
+
name
|
|
216
|
+
description
|
|
217
|
+
template
|
|
218
|
+
}
|
|
219
|
+
catalogEntries
|
|
220
|
+
proposalChoices
|
|
221
|
+
placeholder
|
|
222
|
+
footerNotice
|
|
223
|
+
stagedNotice
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
`
|
|
227
|
+
|
|
228
|
+
/*
|
|
229
|
+
* 방에 말하는 문 — 사람에게 하는 말이고 모델을 부르지 않는다(ADR-0070 A15).
|
|
230
|
+
*
|
|
231
|
+
* 이 문이 서기 전에는 화면에서 나가는 모든 말이 어시스턴트를 향했다. 옆 사람에게 한마디 하려면
|
|
232
|
+
* 어시스턴트가 반드시 대답했고, 그 대답이 다시 대화에 남았다.
|
|
233
|
+
*/
|
|
234
|
+
/*
|
|
235
|
+
* `@` 가 고를 수 있는 사람들 — 이 도메인의 사용자.
|
|
236
|
+
*
|
|
237
|
+
* 호스트가 `userProvider` 를 주지 않으면 여기서 직접 묻는다. 주지 않는 것이 기본이었고, 그래서 앱 셋
|
|
238
|
+
* 어디에서도 `@이름` 을 **고를 수가 없었다** — 멘션이 사람을 불러들이는 기전(ADR-0071 결정 1)을 세워
|
|
239
|
+
* 놓고 부르는 자리가 없는 상태였다. 이 문은 `ai-assistant` 의 것이고 대화창이 그 문의 화면이다.
|
|
240
|
+
*/
|
|
241
|
+
const DOMAIN_USERS_FOR_MENTION_QUERY = gql`
|
|
242
|
+
query DomainUsersForMention($query: String) {
|
|
243
|
+
domainUsersForMention(query: $query) {
|
|
244
|
+
id
|
|
245
|
+
name
|
|
246
|
+
email
|
|
175
247
|
}
|
|
176
248
|
}
|
|
177
249
|
`
|
|
178
250
|
|
|
251
|
+
const SAY_TO_ROOM_MUTATION = gql`
|
|
252
|
+
mutation SayToRoom($sessionId: String!, $content: String!) {
|
|
253
|
+
sayToRoom(sessionId: $sessionId, content: $content)
|
|
254
|
+
}
|
|
255
|
+
`
|
|
256
|
+
|
|
179
257
|
const CHAT_MESSAGES_QUERY = gql`
|
|
180
258
|
query ChatMessages($sessionId: String!, $limit: Int, $offset: Int) {
|
|
181
259
|
chatMessages(sessionId: $sessionId, limit: $limit, offset: $offset) {
|
|
@@ -186,6 +264,7 @@ const CHAT_MESSAGES_QUERY = gql`
|
|
|
186
264
|
toolUsagesJson
|
|
187
265
|
groundingWarningsJson
|
|
188
266
|
createdAt
|
|
267
|
+
redacted
|
|
189
268
|
creator {
|
|
190
269
|
name
|
|
191
270
|
email
|
|
@@ -206,6 +285,7 @@ const CHAT_MESSAGE_SUBSCRIPTION = gql`
|
|
|
206
285
|
toolUsagesJson
|
|
207
286
|
groundingWarningsJson
|
|
208
287
|
createdAt
|
|
288
|
+
redacted
|
|
209
289
|
creator {
|
|
210
290
|
name
|
|
211
291
|
email
|
|
@@ -305,8 +385,10 @@ export class OxAssistantChat extends LitElement {
|
|
|
305
385
|
* `@` mention 도메인 사용자 공급자 (선택). 호스트가 `domainUsersForMention` query 로 wrap.
|
|
306
386
|
*
|
|
307
387
|
* 호스트는 query 문자열을 받아 도메인 사용자 후보 (id/name/email) 배열을 돌려준다.
|
|
308
|
-
*
|
|
309
|
-
*
|
|
388
|
+
*
|
|
389
|
+
* **미설정이면 대화창이 이 패키지의 문(`domainUsersForMention`)에 직접 묻는다.** 예전에는 빈
|
|
390
|
+
* 결과였고, 그래서 앱 셋에서는 `@이름` 을 고를 수 없었다 — 멘션이 사람을 불러들이는 길을 세워도
|
|
391
|
+
* 부르는 자리가 없었다. 자기 목록을 따로 쓰는 호스트만 이것을 준다.
|
|
310
392
|
*/
|
|
311
393
|
@property({ attribute: false })
|
|
312
394
|
userProvider?: (
|
|
@@ -352,34 +434,38 @@ export class OxAssistantChat extends LitElement {
|
|
|
352
434
|
* returns patches and scene actions
|
|
353
435
|
* assistantChat a surface with no document of its own — lives here, runs registered tools only
|
|
354
436
|
*
|
|
355
|
-
*
|
|
356
|
-
*
|
|
357
|
-
*
|
|
358
|
-
*
|
|
359
|
-
* chat calls a mutation that lives in a package it may not even load — which is how
|
|
360
|
-
* operato-figure got `Cannot query field "boardAIChat" on type "Mutation"` (2026-09-11).
|
|
437
|
+
* **There is no default** (ADR-0061 decision 7). It was `boardAIChat`, which meant a surface that
|
|
438
|
+
* said nothing talked to a package it may not even load — operato-figure got
|
|
439
|
+
* `Cannot query field "boardAIChat" on type "Mutation"` that way (2026-09-11). A surface that
|
|
440
|
+
* declares a mode talks to `assistantChat`; one that declares no mode names this itself.
|
|
361
441
|
*/
|
|
362
442
|
@property({ type: String, attribute: 'chat-endpoint' })
|
|
363
|
-
chatEndpoint
|
|
443
|
+
chatEndpoint?: 'boardAIChat' | 'assistantChat'
|
|
364
444
|
|
|
365
|
-
/**
|
|
366
|
-
* Who the assistant is on this surface, in the host's words. Required by `assistantChat`; the
|
|
367
|
-
* board conversation carries its own persona on the server and ignores this.
|
|
368
|
-
*/
|
|
369
445
|
/**
|
|
370
446
|
* What this conversation is — see `AssistantMode`.
|
|
371
447
|
*
|
|
372
|
-
* A host that sets this
|
|
373
|
-
*
|
|
374
|
-
*
|
|
448
|
+
* A host that sets this sets nothing else about the conversation: the persona, the tool
|
|
449
|
+
* categories, the model and the limits belong to the mode its app server registered, and what
|
|
450
|
+
* `@` and `/` offer comes from that same declaration, translated. They cannot disagree because
|
|
451
|
+
* there is one of them.
|
|
375
452
|
*
|
|
376
|
-
* When `
|
|
453
|
+
* When `kind` or `target` changes the conversation on screen is closed and a new one begins.
|
|
377
454
|
*/
|
|
378
455
|
@property({ attribute: false })
|
|
379
456
|
mode?: AssistantMode
|
|
380
457
|
|
|
381
|
-
|
|
382
|
-
|
|
458
|
+
/**
|
|
459
|
+
* 이 화면이 보여 주는 것의 **종류** — `'board'`, `'space'`, `'station'`, `'figure'` (ADR-0072 A17 ①).
|
|
460
|
+
*
|
|
461
|
+
* `mode` 가 「지금 어느 대화인가」라면 이것은 「그 자리에 어떤 대화들이 설 수 있나」이다. 서버가
|
|
462
|
+
* `assistantModes(subject)` 로 답하고, 둘 이상이면 고르개가 뜬다. 하나면 고르개를 내지 않는다 —
|
|
463
|
+
* 고를 것이 없는 고르개는 자리만 차지한다.
|
|
464
|
+
*
|
|
465
|
+
* 화면이 이것을 안 주면 고르개는 없고 host 가 정한 대화 하나만 산다. 그것이 오늘까지의 모양이다.
|
|
466
|
+
*/
|
|
467
|
+
@property({ type: String })
|
|
468
|
+
subject?: string
|
|
383
469
|
|
|
384
470
|
/**
|
|
385
471
|
* 라이브 상태를 다루는 대화면인가 — true 면 **첫 턴에 도구 호출을 강제**한다.
|
|
@@ -408,8 +494,10 @@ export class OxAssistantChat extends LitElement {
|
|
|
408
494
|
@property({ type: Array, attribute: false })
|
|
409
495
|
componentSchemas?: Array<{ type: string; description?: string; group?: string; properties?: Record<string, any> }>
|
|
410
496
|
|
|
497
|
+
/* No default here — the render falls back to `ai-assistant.text.input-placeholder`, which every locale has.
|
|
498
|
+
* A literal default is one language for every reader, and it hid the key that already existed. */
|
|
411
499
|
@property({ type: String })
|
|
412
|
-
placeholder
|
|
500
|
+
placeholder?: string
|
|
413
501
|
|
|
414
502
|
@state()
|
|
415
503
|
private lines: ChatLine[] = []
|
|
@@ -607,11 +695,12 @@ export class OxAssistantChat extends LitElement {
|
|
|
607
695
|
The screen still shows nothing either way. There is no sentence to show, and inventing one is
|
|
608
696
|
what this whole correction removed.
|
|
609
697
|
*/
|
|
610
|
-
if (!this.conversation.
|
|
698
|
+
if (!this.conversation.stagedNotice && !OxAssistantChat.warnedMissingStagedNotice) {
|
|
611
699
|
OxAssistantChat.warnedMissingStagedNotice = true
|
|
612
700
|
console.warn(
|
|
613
|
-
'[assistant-chat] a host reported a proposal staged but
|
|
614
|
-
'
|
|
701
|
+
'[assistant-chat] a host reported a proposal staged but declared no staged notice, so the card can ' +
|
|
702
|
+
'say nothing. Declare `stagedNoticeKey` on the mode (the server translates it), or pass one to ' +
|
|
703
|
+
'`registerChatDefaults` from a host with no mode.'
|
|
615
704
|
)
|
|
616
705
|
}
|
|
617
706
|
|
|
@@ -663,6 +752,10 @@ export class OxAssistantChat extends LitElement {
|
|
|
663
752
|
return (auth.credential as any)?.email
|
|
664
753
|
}
|
|
665
754
|
|
|
755
|
+
private get _myUserId(): string | undefined {
|
|
756
|
+
return (auth.credential as any)?.id
|
|
757
|
+
}
|
|
758
|
+
|
|
666
759
|
/** Mention popup — `#` (컴포넌트) / `@` (카탈로그) / `/` (slash) 트리거 시 활성.
|
|
667
760
|
* 단일 popup 재사용 — mentionTrigger 로 활성 trigger 추적. */
|
|
668
761
|
@state()
|
|
@@ -716,15 +809,114 @@ export class OxAssistantChat extends LitElement {
|
|
|
716
809
|
return resolveConversation(
|
|
717
810
|
this.mode,
|
|
718
811
|
{
|
|
719
|
-
systemPrompt: this.systemPrompt,
|
|
720
|
-
toolCategories: this.toolCategories,
|
|
721
812
|
hostContext: this.hostContext,
|
|
722
|
-
chatEndpoint: this.chatEndpoint
|
|
723
|
-
|
|
724
|
-
|
|
813
|
+
chatEndpoint: this.chatEndpoint,
|
|
814
|
+
/* A host with no mode still registers its wording as keys; it is translated here. */
|
|
815
|
+
footerNotice: chatDefaults.footerNoticeKey ? i18next.t(chatDefaults.footerNoticeKey) : undefined,
|
|
816
|
+
stagedNotice: chatDefaults.stagedNoticeKey ? i18next.t(chatDefaults.stagedNoticeKey) : undefined
|
|
817
|
+
} as any,
|
|
818
|
+
chatDefaults as any,
|
|
819
|
+
this.modeView
|
|
725
820
|
)
|
|
726
821
|
}
|
|
727
822
|
|
|
823
|
+
/**
|
|
824
|
+
* What the server said this mode offers, in this person's language.
|
|
825
|
+
*
|
|
826
|
+
* Fetched when the mode changes and kept until the next one answers. Until it arrives the panel
|
|
827
|
+
* offers nothing rather than the list some other package registered globally.
|
|
828
|
+
*/
|
|
829
|
+
@state() private modeView?: AssistantModeView
|
|
830
|
+
|
|
831
|
+
private modeViewToken = 0
|
|
832
|
+
|
|
833
|
+
/** 이 subject 에 설 수 있는 대화들 — 기본이 먼저. 하나뿐이면 고르개를 그리지 않는다. */
|
|
834
|
+
@state() private modeChoices: { kind: string; label: string; isDefault: boolean }[] = []
|
|
835
|
+
|
|
836
|
+
private modeChoicesToken = 0
|
|
837
|
+
|
|
838
|
+
/**
|
|
839
|
+
* 이 화면에 설 수 있는 대화를 서버에 묻는다.
|
|
840
|
+
*
|
|
841
|
+
* 화면이 종류 목록을 손에 들고 있으면, 대화를 하나 더 세운 날 그 화면만 모른 채로 남는다. 목록은
|
|
842
|
+
* 앱 서버의 선언에서 나온다(ADR-0072 A17 ①).
|
|
843
|
+
*/
|
|
844
|
+
private async loadModeChoices() {
|
|
845
|
+
const subject = this.subject
|
|
846
|
+
if (!subject) {
|
|
847
|
+
this.modeChoices = []
|
|
848
|
+
return
|
|
849
|
+
}
|
|
850
|
+
const token = ++this.modeChoicesToken
|
|
851
|
+
try {
|
|
852
|
+
const result = await client.query({
|
|
853
|
+
query: ASSISTANT_MODES_QUERY,
|
|
854
|
+
variables: { subject },
|
|
855
|
+
fetchPolicy: 'cache-first'
|
|
856
|
+
})
|
|
857
|
+
if (token !== this.modeChoicesToken) return
|
|
858
|
+
this.modeChoices = result.data?.assistantModes ?? []
|
|
859
|
+
} catch {
|
|
860
|
+
/*
|
|
861
|
+
* 못 물어봤다고 화면을 막지 않는다 — host 가 준 대화는 그대로 산다. 고르개만 안 나온다.
|
|
862
|
+
* 여기서 오류 문구를 띄우면 고를 것이 하나뿐인 화면에서도 빨간 줄이 뜬다.
|
|
863
|
+
*/
|
|
864
|
+
if (token !== this.modeChoicesToken) return
|
|
865
|
+
this.modeChoices = []
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
/**
|
|
870
|
+
* 사람이 다른 대화를 골랐다.
|
|
871
|
+
*
|
|
872
|
+
* 고른 것은 **이 subject 종류에 대해** 기억한다 — 보드를 열 때마다 저작 대화로 돌아가면, 현장을
|
|
873
|
+
* 물으려던 사람은 화면을 열 때마다 같은 손질을 다시 해야 한다. 기억은 이 브라우저에만 남는다
|
|
874
|
+
* (서버에 칸을 만들지 않는다 — A17 ①). 세션 자체는 자기가 어느 대화였는지 이미 안다.
|
|
875
|
+
*/
|
|
876
|
+
private onModePick(kind: string) {
|
|
877
|
+
if (!this.mode || kind === this.mode.kind) return
|
|
878
|
+
this.mode = { ...this.mode, kind }
|
|
879
|
+
try {
|
|
880
|
+
if (this.subject) localStorage.setItem(`assistant-mode:${this.subject}`, kind)
|
|
881
|
+
} catch {
|
|
882
|
+
/* 저장이 막힌 브라우저에서도 고르기 자체는 된다 — 다음에 다시 고르면 될 뿐이다. */
|
|
883
|
+
}
|
|
884
|
+
this.dispatchEvent(new CustomEvent('assistant-mode-change', { detail: { kind, subject: this.subject }, bubbles: true, composed: true }))
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
/** 이 subject 에서 지난번에 고른 대화 — 없으면 undefined. */
|
|
888
|
+
private rememberedMode(subject: string): string | undefined {
|
|
889
|
+
try {
|
|
890
|
+
return localStorage.getItem(`assistant-mode:${subject}`) ?? undefined
|
|
891
|
+
} catch {
|
|
892
|
+
return undefined
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
private async loadModeView() {
|
|
897
|
+
const mode = this.mode
|
|
898
|
+
if (!mode?.kind) {
|
|
899
|
+
this.modeView = undefined
|
|
900
|
+
return
|
|
901
|
+
}
|
|
902
|
+
const token = ++this.modeViewToken
|
|
903
|
+
try {
|
|
904
|
+
const result = await client.query({ query: ASSISTANT_MODE_QUERY, variables: { kind: mode.kind }, fetchPolicy: 'cache-first' })
|
|
905
|
+
/* A late answer for a conversation the person has already left is dropped. */
|
|
906
|
+
if (token !== this.modeViewToken) return
|
|
907
|
+
this.modeView = result.data?.assistantMode
|
|
908
|
+
this.rebuildMentionCaches()
|
|
909
|
+
} catch (error) {
|
|
910
|
+
if (token !== this.modeViewToken) return
|
|
911
|
+
this.modeView = undefined
|
|
912
|
+
/*
|
|
913
|
+
* A kind this server does not have is a mismatched deployment, and the person should read it
|
|
914
|
+
* rather than watch an empty `/` list.
|
|
915
|
+
*/
|
|
916
|
+
this.errorMessage = error instanceof Error ? error.message : String(error)
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
|
|
728
920
|
/*
|
|
729
921
|
* The `@` and `/` candidates, built once per mode.
|
|
730
922
|
*
|
|
@@ -775,6 +967,52 @@ export class OxAssistantChat extends LitElement {
|
|
|
775
967
|
}
|
|
776
968
|
|
|
777
969
|
/* ── C4 Roster (participants + presence) ───────────── */
|
|
970
|
+
/* 이 대화의 주어 — 같은 화면 위에 두 대화가 설 때만 나온다(ADR-0072 A17 ①).
|
|
971
|
+
지금 대화가 제목 굵기로 서고, 바꿀 자리가 그 옆에 붙는다. 세션 탭과 같은 무게로 그리지 않는다
|
|
972
|
+
(UX 레인, 2026-09-18) — 한쪽은 화제를 바꾸고 한쪽은 실타래를 바꾼다. */
|
|
973
|
+
.mode-subject {
|
|
974
|
+
display: flex;
|
|
975
|
+
align-items: baseline;
|
|
976
|
+
gap: 8px;
|
|
977
|
+
padding: 6px 12px;
|
|
978
|
+
border-bottom: 1px solid var(--md-sys-color-outline-variant, #e2e8f0);
|
|
979
|
+
}
|
|
980
|
+
.mode-now {
|
|
981
|
+
/* 고른 상태의 색은 세션 탭과 같은 계열이다 — 한 패널 안에서 「골랐다」가 두 색으로 나오면 읽는
|
|
982
|
+
사람은 두 색이 서로 다른 뜻인 줄 안다.
|
|
983
|
+
|
|
984
|
+
12px 이었다가 한 단 내렸다. 도크 제목(figure 의 .scope-title 이 12.5px / 650)과 0.5px ·
|
|
985
|
+
굵기 50 차이는 눈에 닿지 않아서, 거의 같은 제목 줄이 둘 포개졌다. 도크 제목은 그 도크 전체를
|
|
986
|
+
거느리고 이 줄은 그 안 대화 하나의 성질이다 — 11px 이면 이 패널의 다른 조작부와 같은 단이고,
|
|
987
|
+
「이것이 주어다」는 색이 계속 진다(UX 레인, 2026-09-18). */
|
|
988
|
+
color: var(--primary-color, #007f87);
|
|
989
|
+
font-size: 11px;
|
|
990
|
+
font-weight: 600;
|
|
991
|
+
}
|
|
992
|
+
.mode-switch,
|
|
993
|
+
.mode-select {
|
|
994
|
+
appearance: none;
|
|
995
|
+
background: transparent;
|
|
996
|
+
border: 1px solid var(--md-sys-color-outline-variant, #cbd5e1);
|
|
997
|
+
border-radius: 999px;
|
|
998
|
+
color: var(--md-sys-color-on-surface-variant, #475569);
|
|
999
|
+
cursor: pointer;
|
|
1000
|
+
font: inherit;
|
|
1001
|
+
font-size: 11px;
|
|
1002
|
+
padding: 2px 10px;
|
|
1003
|
+
white-space: nowrap;
|
|
1004
|
+
}
|
|
1005
|
+
.mode-switch:hover:not([disabled]) {
|
|
1006
|
+
background: color-mix(in srgb, var(--primary-color, #007f87) 7%, transparent);
|
|
1007
|
+
color: var(--primary-color, #007f87);
|
|
1008
|
+
}
|
|
1009
|
+
/* 답하는 동안에도 「내가 지금 무엇에 대해 말하고 있나」는 또렷해야 한다 — 흐려지는 것은 바꿀
|
|
1010
|
+
자리뿐이다. 지금 대화까지 흐리면 그 한 줄이 가장 안 보인다. */
|
|
1011
|
+
.mode-subject [disabled] {
|
|
1012
|
+
cursor: default;
|
|
1013
|
+
opacity: 0.5;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
778
1016
|
.roster {
|
|
779
1017
|
display: flex;
|
|
780
1018
|
align-items: center;
|
|
@@ -1325,6 +1563,29 @@ export class OxAssistantChat extends LitElement {
|
|
|
1325
1563
|
overflow: visible;
|
|
1326
1564
|
}
|
|
1327
1565
|
|
|
1566
|
+
/* 값이 안 온 줄 — ADR-0073 판정 1.
|
|
1567
|
+
말풍선의 자리와 「AI」 딱지는 지킨다 — 그 줄은 어시스턴트의 답이 맞고, 읽는 사람이 못 지나간
|
|
1568
|
+
것이다. 점선 테두리와 흐린 색이 「이것은 그가 쓴 문장이 아니다」를 말한다.
|
|
1569
|
+
|
|
1570
|
+
기울임은 쓰지 않는다. 이 앱의 글꼴 묶음에 한글 이탤릭 자소가 없어 브라우저가 합성으로 기울이고,
|
|
1571
|
+
획 균형이 무너져 작은 글자가 먼저 안 읽힌다. UX 레인이 :4000 에서 재 보았다 — 「권한」의 진행
|
|
1572
|
+
폭이 normal·italic 둘 다 27.6875px 로 같다(2026-09-18). */
|
|
1573
|
+
.msg.redacted {
|
|
1574
|
+
background: transparent;
|
|
1575
|
+
border: 1px dashed var(--md-sys-color-outline-variant, #cbd5e1);
|
|
1576
|
+
color: var(--md-sys-color-outline, #94a3b8);
|
|
1577
|
+
}
|
|
1578
|
+
.redacted-body {
|
|
1579
|
+
align-items: center;
|
|
1580
|
+
display: flex;
|
|
1581
|
+
gap: 6px;
|
|
1582
|
+
}
|
|
1583
|
+
.redacted-body md-icon {
|
|
1584
|
+
/* md-icon 은 자기 크기를 이 변수로 잡는다 — width/height 만 주면 아무 일도 안 일어난다. */
|
|
1585
|
+
--md-icon-size: 14px;
|
|
1586
|
+
font-size: 14px;
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1328
1589
|
.msg.pending {
|
|
1329
1590
|
opacity: 0.5;
|
|
1330
1591
|
}
|
|
@@ -2185,6 +2446,20 @@ export class OxAssistantChat extends LitElement {
|
|
|
2185
2446
|
cursor: not-allowed;
|
|
2186
2447
|
}
|
|
2187
2448
|
|
|
2449
|
+
/* 보내기 전에 보이는 초대 안내 — 누르기 전에 알아야 하는 사실이다. */
|
|
2450
|
+
.invite-notice {
|
|
2451
|
+
display: flex;
|
|
2452
|
+
align-items: center;
|
|
2453
|
+
gap: 6px;
|
|
2454
|
+
margin: 0 2px 4px;
|
|
2455
|
+
color: var(--md-sys-color-on-surface-variant, #475569);
|
|
2456
|
+
font-size: 11.5px;
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2459
|
+
.invite-notice md-icon {
|
|
2460
|
+
--md-icon-size: 15px;
|
|
2461
|
+
}
|
|
2462
|
+
|
|
2188
2463
|
/* ── Send button — ChatGPT 스타일 원형 ↑ (.input-row 안의 button 만) ─── */
|
|
2189
2464
|
.input-row > button {
|
|
2190
2465
|
width: 32px;
|
|
@@ -2206,6 +2481,15 @@ export class OxAssistantChat extends LitElement {
|
|
|
2206
2481
|
align-self: flex-end;
|
|
2207
2482
|
margin-bottom: 2px;
|
|
2208
2483
|
}
|
|
2484
|
+
/*
|
|
2485
|
+
방에 말하는 자리는 **두 번째 행동**이다 — 이 화면의 첫 행동은 어시스턴트에게 묻는 것이다.
|
|
2486
|
+
같은 무게로 두면 무엇이 기본인지 화면이 말하지 않게 된다(ADR-0070 A15 의 자리 둘).
|
|
2487
|
+
*/
|
|
2488
|
+
.input-row > button.say-to-room {
|
|
2489
|
+
background: var(--md-sys-color-surface-container-highest, #e2e8f0);
|
|
2490
|
+
color: var(--md-sys-color-on-surface-variant, #475569);
|
|
2491
|
+
}
|
|
2492
|
+
|
|
2209
2493
|
.input-row > button:disabled {
|
|
2210
2494
|
background: var(--md-sys-color-surface-container-highest, #e2e8f0);
|
|
2211
2495
|
color: var(--md-sys-color-outline, #94a3b8);
|
|
@@ -2241,6 +2525,23 @@ export class OxAssistantChat extends LitElement {
|
|
|
2241
2525
|
// 첫 마운트 시 sessionId 가 이미 set 돼있어도 loadHistory 는 첫 render 후 updated()
|
|
2242
2526
|
// 가 처리 — 단일 진입점으로 일관 (이전엔 connectedCallback 과 updated 양쪽이 호출 →
|
|
2243
2527
|
// 동시 fetch race 발생).
|
|
2528
|
+
/* 모드가 이미 붙어 있으면 무엇을 주는지 먼저 묻는다 — 바뀔 때는 updated 가 다시 묻는다. */
|
|
2529
|
+
if (this.mode?.kind) this.loadModeView()
|
|
2530
|
+
if (this.subject) this.onSubjectChanged()
|
|
2531
|
+
}
|
|
2532
|
+
|
|
2533
|
+
/**
|
|
2534
|
+
* 화면이 보여 주는 것의 종류가 바뀌었다 — 설 수 있는 대화를 다시 묻고, 지난번 고른 것을 되살린다.
|
|
2535
|
+
*
|
|
2536
|
+
* 되살리는 것은 **고른 대화**이지 세션이 아니다. 세션은 자기가 어느 대화였는지 이미 알고, 다른
|
|
2537
|
+
* 대화로 가면 그때 새로 난다(ADR-0072 A17 ①).
|
|
2538
|
+
*/
|
|
2539
|
+
private async onSubjectChanged() {
|
|
2540
|
+
await this.loadModeChoices()
|
|
2541
|
+
if (this.modeChoices.length < 2 || !this.subject || !this.mode) return
|
|
2542
|
+
|
|
2543
|
+
const kind = pickInitialMode(this.mode.kind, this.modeChoices, this.rememberedMode(this.subject))
|
|
2544
|
+
if (kind && kind !== this.mode.kind) this.mode = { ...this.mode, kind }
|
|
2244
2545
|
}
|
|
2245
2546
|
|
|
2246
2547
|
updated(changed: Map<string, any>) {
|
|
@@ -2268,8 +2569,14 @@ export class OxAssistantChat extends LitElement {
|
|
|
2268
2569
|
* The host still owns `sessionId`; it hears `assistant-mode-changed` and decides whether
|
|
2269
2570
|
* this subject gets a stored session of its own.
|
|
2270
2571
|
*/
|
|
2572
|
+
/* 보는 것의 종류가 바뀌면 설 수 있는 대화도 달라진다 — 보드에서 현장으로 넘어간 자리. */
|
|
2573
|
+
if (changed.has('subject')) this.onSubjectChanged()
|
|
2574
|
+
|
|
2271
2575
|
if (changed.has('mode') && isDifferentConversation(changed.get('mode'), this.mode)) {
|
|
2576
|
+
/* What this conversation offers is the new mode's; ask before drawing the panel again. */
|
|
2577
|
+
this.modeView = undefined
|
|
2272
2578
|
this.rebuildMentionCaches()
|
|
2579
|
+
this.loadModeView()
|
|
2273
2580
|
this.lines = []
|
|
2274
2581
|
this.pickedMentions.clear()
|
|
2275
2582
|
this.pickedUserMentions.clear()
|
|
@@ -2319,6 +2626,10 @@ export class OxAssistantChat extends LitElement {
|
|
|
2319
2626
|
disconnectedCallback() {
|
|
2320
2627
|
this._stopPresence()
|
|
2321
2628
|
this._stopMessageSubscription()
|
|
2629
|
+
if (this.elapsedTimer) {
|
|
2630
|
+
clearInterval(this.elapsedTimer)
|
|
2631
|
+
this.elapsedTimer = undefined
|
|
2632
|
+
}
|
|
2322
2633
|
super.disconnectedCallback()
|
|
2323
2634
|
}
|
|
2324
2635
|
|
|
@@ -2455,6 +2766,7 @@ export class OxAssistantChat extends LitElement {
|
|
|
2455
2766
|
${this.toastMessage
|
|
2456
2767
|
? html`<div class="toast show">${this.toastMessage}</div>`
|
|
2457
2768
|
: nothing}
|
|
2769
|
+
${this.renderModeSubject()}
|
|
2458
2770
|
${this.renderSessionTabs()}
|
|
2459
2771
|
${this.renderRoster()}
|
|
2460
2772
|
<div class="messages" @click=${this.onBubbleClick}>
|
|
@@ -2472,8 +2784,12 @@ export class OxAssistantChat extends LitElement {
|
|
|
2472
2784
|
const prevSameSender = !!prev && prev.role === 'user' && prev.senderEmail === line.senderEmail
|
|
2473
2785
|
const showSenderLabel = isOther && !prevSameSender
|
|
2474
2786
|
const content = (line.content ?? '').trim()
|
|
2475
|
-
|
|
2476
|
-
|
|
2787
|
+
/*
|
|
2788
|
+
* 가려진 줄은 어시스턴트의 말이 아니다 — 마크다운으로 그리지 않고, 복사·재실행 같은 손잡이도
|
|
2789
|
+
* 달지 않는다(ADR-0073 판정 1). 그 손잡이들은 있지도 않은 답을 다룬다.
|
|
2790
|
+
*/
|
|
2791
|
+
const isMarkdown = line.role !== 'system' && content.length > 0 && !line.redacted
|
|
2792
|
+
const showActions = line.role !== 'system' && !line.pending && content.length > 0 && !line.redacted
|
|
2477
2793
|
return html`
|
|
2478
2794
|
${showLabel
|
|
2479
2795
|
? html`<div class="msg-label">${i18next.t('ai-assistant.label.ai')}</div>`
|
|
@@ -2484,9 +2800,11 @@ export class OxAssistantChat extends LitElement {
|
|
|
2484
2800
|
<div
|
|
2485
2801
|
class="msg ${line.role} ${isOther ? 'other' : ''} ${line.pending ? 'pending' : ''} ${line.systemOpen
|
|
2486
2802
|
? 'open'
|
|
2487
|
-
: ''}">
|
|
2803
|
+
: ''} ${line.redacted ? 'redacted' : ''}">
|
|
2488
2804
|
${line.pending
|
|
2489
2805
|
? html`<span class="typing-dots"><span></span><span></span><span></span></span>`
|
|
2806
|
+
: line.redacted
|
|
2807
|
+
? html`<div class="redacted-body"><md-icon>lock</md-icon><span>${content}</span></div>`
|
|
2490
2808
|
: isMarkdown
|
|
2491
2809
|
? html`<div class="md-body">${unsafeHTML(renderMarkdown(content))}</div>`
|
|
2492
2810
|
: line.role === 'system'
|
|
@@ -2647,6 +2965,7 @@ export class OxAssistantChat extends LitElement {
|
|
|
2647
2965
|
</div>
|
|
2648
2966
|
`
|
|
2649
2967
|
: nothing}
|
|
2968
|
+
${this.renderInviteNotice()}
|
|
2650
2969
|
<div class="input-row">
|
|
2651
2970
|
<div class="textarea-wrap">
|
|
2652
2971
|
<textarea
|
|
@@ -2660,7 +2979,9 @@ export class OxAssistantChat extends LitElement {
|
|
|
2660
2979
|
}}
|
|
2661
2980
|
?disabled=${this.busy}
|
|
2662
2981
|
rows="1"
|
|
2663
|
-
placeholder=${this.placeholder ??
|
|
2982
|
+
placeholder=${this.conversation.placeholder ??
|
|
2983
|
+
this.placeholder ??
|
|
2984
|
+
i18next.t('ai-assistant.text.input-placeholder')}></textarea>
|
|
2664
2985
|
${this.mentionOpen
|
|
2665
2986
|
? html`
|
|
2666
2987
|
<ox-mention-popup
|
|
@@ -2681,6 +3002,27 @@ export class OxAssistantChat extends LitElement {
|
|
|
2681
3002
|
`
|
|
2682
3003
|
: nothing}
|
|
2683
3004
|
</div>
|
|
3005
|
+
${/*
|
|
3006
|
+
두 자리다 — 어시스턴트에게 묻는 자리와 **방에 말하는 자리**. 누른 자리가 그 말의 대상을
|
|
3007
|
+
정하고, 서버는 어느 문이 불렸는지로 그 사실을 저장한다(ADR-0070 A15 결정 ①).
|
|
3008
|
+
|
|
3009
|
+
**방에 사람이 있을 때만 보인다.** 혼자 쓰는 대화에서 「방에 말하기」는 아무에게도 하지 않는
|
|
3010
|
+
말이고, 누를 일이 없는 단추는 무엇이 기본인지 흐린다. 조건은 참여자 목록과 같다(둘 이상) —
|
|
3011
|
+
둘째 사람은 멘션으로 들어오고, 그 순간 이 자리도 함께 선다.
|
|
3012
|
+
*/ ''}
|
|
3013
|
+
${this.sessionId && this.participants.length >= 2
|
|
3014
|
+
? html`
|
|
3015
|
+
<button
|
|
3016
|
+
class="say-to-room"
|
|
3017
|
+
?disabled=${this.busy || !this.input.trim()}
|
|
3018
|
+
@click=${() => this.sayToRoom()}
|
|
3019
|
+
title=${i18next.t('ai-assistant.text.say-to-room-tooltip', {
|
|
3020
|
+
defaultValue: 'say this to the people here — the assistant does not answer'
|
|
3021
|
+
})}>
|
|
3022
|
+
<md-icon>forum</md-icon>
|
|
3023
|
+
</button>
|
|
3024
|
+
`
|
|
3025
|
+
: nothing}
|
|
2684
3026
|
<button
|
|
2685
3027
|
?disabled=${this.busy || (!this.input.trim() && this.attachments.length === 0)}
|
|
2686
3028
|
@click=${this.send}
|
|
@@ -2694,6 +3036,83 @@ export class OxAssistantChat extends LitElement {
|
|
|
2694
3036
|
`
|
|
2695
3037
|
}
|
|
2696
3038
|
|
|
3039
|
+
/**
|
|
3040
|
+
* 방에 말한다 — 어시스턴트는 답하지 않는다.
|
|
3041
|
+
*
|
|
3042
|
+
* 저장되는 대화가 있을 때만 할 수 있다. 세션이 없는 즉석 질문에는 「방」이 없다 — 그 자리에서
|
|
3043
|
+
* 한 말은 누구에게도 남지 않는다.
|
|
3044
|
+
*/
|
|
3045
|
+
private async sayToRoom() {
|
|
3046
|
+
const body = this.input.trim()
|
|
3047
|
+
if (!body || this.busy) return
|
|
3048
|
+
if (!this.sessionId) {
|
|
3049
|
+
/* 말할 방이 없다는 사실을 말한다 — 조용히 버리면 사람은 말했다고 믿는다. */
|
|
3050
|
+
this.errorMessage = i18next.t('ai-assistant.text.no-room-yet', {
|
|
3051
|
+
defaultValue: 'this conversation is not saved yet, so there is nobody else in it to talk to'
|
|
3052
|
+
})
|
|
3053
|
+
return
|
|
3054
|
+
}
|
|
3055
|
+
|
|
3056
|
+
const localId = `local-${++this._localSeq}-r`
|
|
3057
|
+
this.lines = [...this.lines, { role: 'user', content: body, _localId: localId, senderEmail: this._myEmail }]
|
|
3058
|
+
this.input = ''
|
|
3059
|
+
this.busy = true
|
|
3060
|
+
try {
|
|
3061
|
+
const result = await client.mutate({
|
|
3062
|
+
mutation: SAY_TO_ROOM_MUTATION,
|
|
3063
|
+
variables: { sessionId: this.sessionId, content: body }
|
|
3064
|
+
})
|
|
3065
|
+
/* 방송 에코와 겹치지 않게 서버 id 로 맞춘다 — 보낸 말이 두 줄로 보이지 않게. */
|
|
3066
|
+
this._reconcileSentLine(localId, result.data?.sayToRoom ?? undefined, {})
|
|
3067
|
+
} catch (error) {
|
|
3068
|
+
this.lines = this.lines.filter(line => line._localId !== localId)
|
|
3069
|
+
this.input = body
|
|
3070
|
+
this.errorMessage = error instanceof Error ? error.message : String(error)
|
|
3071
|
+
} finally {
|
|
3072
|
+
this.busy = false
|
|
3073
|
+
}
|
|
3074
|
+
}
|
|
3075
|
+
|
|
3076
|
+
/**
|
|
3077
|
+
* 보내기 전에 말한다 — 「이 사람은 이 대화에 들어옵니다」(ADR-0071 결정 1 ④).
|
|
3078
|
+
*
|
|
3079
|
+
* 멘션은 사람을 불러들인다. 누른 뒤에 알면 이미 들어와 있고, 협의의 공개 범위는 되돌리기 어려운
|
|
3080
|
+
* 종류의 일이다. 그래서 **아직 보내지 않은 상태에서** 누가 들어오게 되는지 보인다.
|
|
3081
|
+
*
|
|
3082
|
+
* 초대할 수 없는 자리(보기 권한 · 아직 참여자가 아님)라면 그 사실을 말한다 — 눌렀는데 아무 일도
|
|
3083
|
+
* 일어나지 않는 것이 가장 나쁘다.
|
|
3084
|
+
*/
|
|
3085
|
+
private renderInviteNotice() {
|
|
3086
|
+
if (!this.sessionId) return nothing
|
|
3087
|
+
|
|
3088
|
+
const here = new Set(this.participants.map(p => p.user?.id).filter(Boolean) as string[])
|
|
3089
|
+
const incoming = [...this.pickedUserMentions.entries()]
|
|
3090
|
+
.filter(([token, userId]) => this.input.includes(`@${token}`) && !here.has(userId))
|
|
3091
|
+
.map(([token]) => token)
|
|
3092
|
+
if (incoming.length === 0) return nothing
|
|
3093
|
+
|
|
3094
|
+
const myRole = this.participants.find(p => p.user?.id && p.user.id === this._myUserId)?.role
|
|
3095
|
+
const mayInvite = myRole === 'owner' || myRole === 'member'
|
|
3096
|
+
const names = incoming.map(token => `@${token}`).join(', ')
|
|
3097
|
+
|
|
3098
|
+
return html`
|
|
3099
|
+
<div class="invite-notice">
|
|
3100
|
+
<md-icon>person_add</md-icon>
|
|
3101
|
+
<span>
|
|
3102
|
+
${mayInvite
|
|
3103
|
+
? i18next.t('ai-assistant.text.will-be-invited', {
|
|
3104
|
+
names,
|
|
3105
|
+
defaultValue: '{names} will join this conversation when you send'
|
|
3106
|
+
})
|
|
3107
|
+
: i18next.t('ai-assistant.text.cannot-invite', {
|
|
3108
|
+
names,
|
|
3109
|
+
defaultValue: '{names} will not join — only the people already in this conversation can bring someone in'
|
|
3110
|
+
})}
|
|
3111
|
+
</span>
|
|
3112
|
+
</div>
|
|
3113
|
+
`
|
|
3114
|
+
}
|
|
3115
|
+
|
|
2697
3116
|
private onKeyDown(e: KeyboardEvent) {
|
|
2698
3117
|
// Mention popup 활성 시 — 화살표/Enter/Tab/Esc 가로채서 popup 네비게이션
|
|
2699
3118
|
if (this.mentionOpen) {
|
|
@@ -2815,13 +3234,32 @@ export class OxAssistantChat extends LitElement {
|
|
|
2815
3234
|
this.highlightMatchedCandidates()
|
|
2816
3235
|
}
|
|
2817
3236
|
|
|
3237
|
+
/**
|
|
3238
|
+
* 호스트가 목록을 주지 않을 때 이 도메인의 사용자를 직접 묻는다.
|
|
3239
|
+
*
|
|
3240
|
+
* 실패는 빈 목록이다 — 멘션을 못 고르는 것이지 대화가 막힐 일은 아니다.
|
|
3241
|
+
*/
|
|
3242
|
+
private async fetchDomainUsers(query: string): Promise<Array<{ id: string; name?: string | null; email?: string | null }>> {
|
|
3243
|
+
try {
|
|
3244
|
+
const result = await client.query({
|
|
3245
|
+
query: DOMAIN_USERS_FOR_MENTION_QUERY,
|
|
3246
|
+
variables: { query },
|
|
3247
|
+
fetchPolicy: 'network-only'
|
|
3248
|
+
})
|
|
3249
|
+
return result.data?.domainUsersForMention ?? []
|
|
3250
|
+
} catch {
|
|
3251
|
+
return []
|
|
3252
|
+
}
|
|
3253
|
+
}
|
|
3254
|
+
|
|
2818
3255
|
/** `@` query 에 대한 userProvider 비동기 호출 — 응답 도착 시 popup 결과 갱신.
|
|
2819
3256
|
* race 방지: token 으로 최신 호출 식별 (늦게 도착한 응답 무시). */
|
|
2820
3257
|
private async refreshUserCandidatesAsync(query: string, hashPos: number) {
|
|
2821
|
-
if (!this.userProvider) return
|
|
2822
3258
|
const token = ++this.userFetchToken
|
|
2823
3259
|
try {
|
|
2824
|
-
const users =
|
|
3260
|
+
const users = this.userProvider
|
|
3261
|
+
? await Promise.resolve(this.userProvider(query))
|
|
3262
|
+
: await this.fetchDomainUsers(query)
|
|
2825
3263
|
if (token !== this.userFetchToken) return // 더 최신 호출이 있음
|
|
2826
3264
|
// popup 이 닫혔거나 trigger / hashPos 가 바뀌면 무시
|
|
2827
3265
|
if (!this.mentionOpen || this.mentionTrigger !== '@' || this.mentionHashPos !== hashPos) {
|
|
@@ -3051,6 +3489,63 @@ export class OxAssistantChat extends LitElement {
|
|
|
3051
3489
|
|
|
3052
3490
|
/** 보드의 세션 탭 — 다중 세션 활성화 시 패널 상단. host 가 sessions 비워두면 표시 X.
|
|
3053
3491
|
* 레이블은 session.name 우선, 없으면 fallback "세션 N". */
|
|
3492
|
+
/**
|
|
3493
|
+
* 이 대화의 **주어** — 같은 화면 위에 두 대화가 설 때만 나온다(ADR-0072 A17 ①).
|
|
3494
|
+
*
|
|
3495
|
+
* 하나뿐이면 아무것도 그리지 않는다. 고를 것이 없는 고르개는 자리만 차지하고, 사람에게 「무언가
|
|
3496
|
+
* 고를 것이 있다」는 잘못된 신호를 준다.
|
|
3497
|
+
*
|
|
3498
|
+
* ── 왜 탭 줄이 아닌가 (UX 레인, 2026-09-18) ───────────────────────────────
|
|
3499
|
+
* 처음에는 칩 줄에 `role="tablist"` 를 달았다. 바로 아래가 세션 탭이고 그쪽도 tablist 라서, 화면
|
|
3500
|
+
* 낭독기로 들으면 「탭 목록, 대화」 다음에 「탭 목록, Conversation history」 가 이어졌다 — 고르는
|
|
3501
|
+
* 축이 둘인데 둘 다 「탭」이라고 말한다. 게다가 ARIA 탭은 좌우 화살표 이동과 `tabpanel` 이 함께
|
|
3502
|
+
* 있어야 성립하고, 둘 다 없으면 낭독기는 「2 중 1번째 탭」이라 말하고 화살표는 아무 일도 안 한다.
|
|
3503
|
+
*
|
|
3504
|
+
* 무게도 다르다. 세션은 「어느 지난 대화냐」이고 모드는 「이 대화가 무엇에 대한 것이냐」이다. 같은
|
|
3505
|
+
* 무게로 그리면 어느 쪽이 화제를 바꾸고 어느 쪽이 실타래를 바꾸는지 구별되지 않는다. 그래서 지금
|
|
3506
|
+
* 대화가 제목 굵기로 서고, 바꿀 자리는 그 옆에 붙는다.
|
|
3507
|
+
*
|
|
3508
|
+
* ── 왜 도크 제목 줄로 안 올리나 (UX 레인 판정, 2026-09-18) ────────────────
|
|
3509
|
+
* 도크 제목은 「지금 어디에 있나」(이 도형 · 이 현장 · 이 스테이션 화면)이고 이 줄은 「이 대화가
|
|
3510
|
+
* 무엇에 대한 것이냐」이다. 대화의 주어는 대화 안에 있다. 그리고 세 도크가 각자 그리면 세 벌이 되고
|
|
3511
|
+
* 그 세 벌은 갈린다 — `modeChoices` 를 아는 것도 여기다. 올리면 목록과 고른 것이 세 자리로 나갔다
|
|
3512
|
+
* 돌아온다.
|
|
3513
|
+
*/
|
|
3514
|
+
private renderModeSubject() {
|
|
3515
|
+
if (this.modeChoices.length < 2) return nothing
|
|
3516
|
+
|
|
3517
|
+
const current = this.modeChoices.find(choice => choice.kind === this.mode?.kind)
|
|
3518
|
+
const others = this.modeChoices.filter(choice => choice.kind !== this.mode?.kind)
|
|
3519
|
+
|
|
3520
|
+
return html`
|
|
3521
|
+
<div class="mode-subject">
|
|
3522
|
+
<span class="mode-now">${current?.label ?? this.mode?.kind ?? ''}</span>
|
|
3523
|
+
${others.length === 1
|
|
3524
|
+
? html`
|
|
3525
|
+
<button
|
|
3526
|
+
class="mode-switch"
|
|
3527
|
+
title=${i18next.t('ai-assistant.text.switch-conversation', { name: others[0].label })}
|
|
3528
|
+
aria-label=${i18next.t('ai-assistant.text.switch-conversation', { name: others[0].label })}
|
|
3529
|
+
?disabled=${this.busy}
|
|
3530
|
+
@click=${() => this.onModePick(others[0].kind)}>
|
|
3531
|
+
${others[0].label}
|
|
3532
|
+
</button>
|
|
3533
|
+
`
|
|
3534
|
+
: html`
|
|
3535
|
+
<select
|
|
3536
|
+
class="mode-select"
|
|
3537
|
+
aria-label=${i18next.t('ai-assistant.label.conversation')}
|
|
3538
|
+
?disabled=${this.busy}
|
|
3539
|
+
@change=${(event: Event) => this.onModePick((event.target as HTMLSelectElement).value)}>
|
|
3540
|
+
${this.modeChoices.map(
|
|
3541
|
+
choice => html`<option value=${choice.kind} ?selected=${choice.kind === this.mode?.kind}>${choice.label}</option>`
|
|
3542
|
+
)}
|
|
3543
|
+
</select>
|
|
3544
|
+
`}
|
|
3545
|
+
</div>
|
|
3546
|
+
`
|
|
3547
|
+
}
|
|
3548
|
+
|
|
3054
3549
|
private renderSessionTabs() {
|
|
3055
3550
|
if (!this.sessions || this.sessions.length === 0) return nothing
|
|
3056
3551
|
return html`
|
|
@@ -3154,8 +3649,8 @@ export class OxAssistantChat extends LitElement {
|
|
|
3154
3649
|
<span class="badge">${i18next.t('ai-assistant.label.korean-supported')}</span>
|
|
3155
3650
|
<span class="badge">${i18next.t('ai-assistant.label.multi-command')}</span>
|
|
3156
3651
|
<span class="badge">${i18next.t('ai-assistant.label.review-able')}</span>
|
|
3157
|
-
${this.conversation.
|
|
3158
|
-
? html`<br />${
|
|
3652
|
+
${this.conversation.footerNotice
|
|
3653
|
+
? html`<br />${this.conversation.footerNotice}`
|
|
3159
3654
|
: ''}
|
|
3160
3655
|
</div>
|
|
3161
3656
|
</div>
|
|
@@ -3334,8 +3829,9 @@ export class OxAssistantChat extends LitElement {
|
|
|
3334
3829
|
class="run"
|
|
3335
3830
|
?disabled=${sent}
|
|
3336
3831
|
@click=${() => this.executeProposal(p, key)}>
|
|
3337
|
-
${sent
|
|
3338
|
-
|
|
3832
|
+
${sent
|
|
3833
|
+
? this.proposalStateText(key)
|
|
3834
|
+
: i18next.t('ai-assistant.button.run-proposal', { defaultValue: 'run' })}
|
|
3339
3835
|
</button>
|
|
3340
3836
|
${/*
|
|
3341
3837
|
Drawn only where the host has said it staged this candidate, in the host's own
|
|
@@ -3343,15 +3839,15 @@ export class OxAssistantChat extends LitElement {
|
|
|
3343
3839
|
This badge used to read "there are chips" as "the model is in the 3D viewer" and
|
|
3344
3840
|
say so, in a component that plant and twin also use.
|
|
3345
3841
|
*/ ''}
|
|
3346
|
-
${this.stagedProposals.has(key) && this.conversation.
|
|
3842
|
+
${this.stagedProposals.has(key) && this.conversation.stagedNotice ? html`
|
|
3347
3843
|
<span class="staged-badge">
|
|
3348
3844
|
<md-icon>check_circle</md-icon>
|
|
3349
|
-
<span>${
|
|
3845
|
+
<span>${this.conversation.stagedNotice}</span>
|
|
3350
3846
|
</span>
|
|
3351
3847
|
` : nothing}
|
|
3352
3848
|
</div>
|
|
3353
3849
|
${rawChoices.length ? html`
|
|
3354
|
-
<div class="proposal-choices" role="group" aria-label
|
|
3850
|
+
<div class="proposal-choices" role="group" aria-label=${i18next.t('ai-assistant.text.proposal-choices', { defaultValue: 'follow-up choices' })}>
|
|
3355
3851
|
${rawChoices.map((c: any) => html`
|
|
3356
3852
|
<button
|
|
3357
3853
|
class="choice-chip ${c.primary ? 'primary' : ''}"
|
|
@@ -3369,6 +3865,51 @@ export class OxAssistantChat extends LitElement {
|
|
|
3369
3865
|
`
|
|
3370
3866
|
}
|
|
3371
3867
|
|
|
3868
|
+
/**
|
|
3869
|
+
* 눌린 카드가 말하는 것 — **호스트가 한 일**이고, 아직 모르면 모른다고 말한다.
|
|
3870
|
+
*
|
|
3871
|
+
* 예전에는 누른 순간 「실행됨」이었다. 09-18 트윈에서 아무 일도 일어나지 않은 채 「✓ 반영했습니다」가
|
|
3872
|
+
* 떴다(ADR-0070 A12-a).
|
|
3873
|
+
*/
|
|
3874
|
+
private proposalStateText(key: string): string {
|
|
3875
|
+
const outcome = this.proposalOutcomes.get(key)
|
|
3876
|
+
|
|
3877
|
+
if (!outcome) {
|
|
3878
|
+
/* 답이 오기 전 — 아는 사실은 「보냈고 아직 답이 없다」뿐이다. 경과 시간을 함께 보인다. */
|
|
3879
|
+
const since = this.proposalSentAt.get(key)
|
|
3880
|
+
const seconds = since ? Math.max(0, Math.round((Date.now() - since) / 1000)) : 0
|
|
3881
|
+
return i18next.t('ai-assistant.text.proposal-waiting', {
|
|
3882
|
+
seconds,
|
|
3883
|
+
defaultValue: 'sending — {seconds}s, no answer yet'
|
|
3884
|
+
})
|
|
3885
|
+
}
|
|
3886
|
+
|
|
3887
|
+
const counted =
|
|
3888
|
+
typeof outcome.applied === 'number' && typeof outcome.total === 'number' && outcome.total > 0
|
|
3889
|
+
? i18next.t('ai-assistant.text.proposal-applied-count', {
|
|
3890
|
+
applied: outcome.applied,
|
|
3891
|
+
total: outcome.total,
|
|
3892
|
+
defaultValue: ' {applied} of {total}'
|
|
3893
|
+
})
|
|
3894
|
+
: ''
|
|
3895
|
+
|
|
3896
|
+
/* 사유는 호스트가 준 코드 그대로다 — 문장으로 바꾸지 않는다(ADR-0042). */
|
|
3897
|
+
const reason = outcome.errorCode ? ` (${outcome.errorCode})` : ''
|
|
3898
|
+
|
|
3899
|
+
const verdict = i18next.t(`ai-assistant.verdict.${outcome.verdict}`, {
|
|
3900
|
+
defaultValue:
|
|
3901
|
+
outcome.verdict === 'accepted'
|
|
3902
|
+
? 'done'
|
|
3903
|
+
: outcome.verdict === 'refused'
|
|
3904
|
+
? 'refused'
|
|
3905
|
+
: outcome.verdict === 'declined'
|
|
3906
|
+
? 'cancelled'
|
|
3907
|
+
: 'could not run'
|
|
3908
|
+
})
|
|
3909
|
+
|
|
3910
|
+
return `${verdict}${counted}${reason}`
|
|
3911
|
+
}
|
|
3912
|
+
|
|
3372
3913
|
private onChoiceClick(choice: any, proposal: any, key: string) {
|
|
3373
3914
|
if (choice.action === 'save') {
|
|
3374
3915
|
this.dispatchEvent(
|
|
@@ -3420,10 +3961,17 @@ export class OxAssistantChat extends LitElement {
|
|
|
3420
3961
|
.map(k => `${JSON.stringify(k)}:${stable(v[k])}`)
|
|
3421
3962
|
.join(',')}}`
|
|
3422
3963
|
}
|
|
3964
|
+
/*
|
|
3965
|
+
* 서버가 실어 보낸 키가 정본이다 — 무엇이 같은 조치인가는 제안을 만든 쪽이 안다(ADR-0061 결정
|
|
3966
|
+
* 7 뒤 정리). 예전에는 여기서 `command|instanceId|args` 로 다시 셌는데, 명령이 없는 제안(선언 ·
|
|
3967
|
+
* 새 트윈 초안)은 그 셋이 모두 비어 **서로 다른 제안이 한 카드로 접혔고**, 하나를 누르면 나머지가
|
|
3968
|
+
* 함께 「요청됨」이 됐다.
|
|
3969
|
+
*/
|
|
3970
|
+
if (typeof p?.key === 'string' && p.key) return p.key
|
|
3423
3971
|
try {
|
|
3424
|
-
return `${p?.command ?? ''}|${p?.instanceId ?? ''}|${stable(p?.args)}`
|
|
3972
|
+
return `${p?.tool ?? ''}|${p?.command ?? ''}|${p?.instanceId ?? ''}|${stable(p?.args)}|${stable(p?.ops)}`
|
|
3425
3973
|
} catch {
|
|
3426
|
-
return `${p?.command ?? ''}|${p?.instanceId ?? ''}`
|
|
3974
|
+
return `${p?.tool ?? ''}|${p?.command ?? ''}|${p?.instanceId ?? ''}`
|
|
3427
3975
|
}
|
|
3428
3976
|
}
|
|
3429
3977
|
|
|
@@ -3431,17 +3979,64 @@ export class OxAssistantChat extends LitElement {
|
|
|
3431
3979
|
* 실행은 호스트가 한다 — 이 컴포넌트는 도메인(트윈 명령 채널)을 모른다.
|
|
3432
3980
|
* 확인 대화·명령 전송·결과 기록은 호스트의 몫이고, 여기서는 중복 클릭만 막는다.
|
|
3433
3981
|
*/
|
|
3982
|
+
/**
|
|
3983
|
+
* 누른 시각 — 「보내는 중」 옆의 경과 시간용.
|
|
3984
|
+
*
|
|
3985
|
+
* 시간으로 실패를 판정하지 않는다(ADR-0070 A12-a 결정 ②). 아는 사실은 「아직 답을 받지 못했다」
|
|
3986
|
+
* 하나이고, 시간이 지났다고 실패로 바꾸면 그것이 지어낸 원인이다.
|
|
3987
|
+
*/
|
|
3988
|
+
private proposalSentAt = new Map<string, number>()
|
|
3989
|
+
|
|
3990
|
+
private elapsedTimer?: ReturnType<typeof setInterval>
|
|
3991
|
+
|
|
3434
3992
|
private executeProposal(p: any, key: string) {
|
|
3435
3993
|
this.sentProposals = new Set(this.sentProposals).add(key)
|
|
3994
|
+
this.proposalSentAt.set(key, Date.now())
|
|
3995
|
+
this._startElapsedTicker()
|
|
3436
3996
|
this.dispatchEvent(
|
|
3437
3997
|
new CustomEvent('assistant-proposal-execute', {
|
|
3438
|
-
|
|
3998
|
+
/*
|
|
3999
|
+
* `proposalKey` 를 함께 준다 — 호스트가 `reportProposalOutcome` 으로 답할 때 쓰는 이름이다.
|
|
4000
|
+
* 호스트가 자기 규칙으로 키를 다시 만들면 카드가 못 찾는 답이 생긴다(원본은 하나).
|
|
4001
|
+
*/
|
|
4002
|
+
detail: { proposal: p, sessionId: this.sessionId, proposalKey: key },
|
|
3439
4003
|
bubbles: true,
|
|
3440
4004
|
composed: true
|
|
3441
4005
|
})
|
|
3442
4006
|
)
|
|
3443
4007
|
}
|
|
3444
4008
|
|
|
4009
|
+
/**
|
|
4010
|
+
* 호스트가 무엇을 했는지 — 대화에 남은 `system` 행에서 읽는다(ADR-0070 A12-a 결정 ①).
|
|
4011
|
+
*
|
|
4012
|
+
* 카드가 자기 기억으로 상태를 들고 있으면 새로 고치는 순간 사라지고, 다른 창에서 실행한 것은
|
|
4013
|
+
* 애초에 보이지 않는다. 그래서 상태의 자리는 이력이다.
|
|
4014
|
+
*/
|
|
4015
|
+
private get proposalOutcomes(): Map<string, ParsedProposalOutcome> {
|
|
4016
|
+
const outcomes = new Map<string, ParsedProposalOutcome>()
|
|
4017
|
+
for (const line of this.lines) {
|
|
4018
|
+
if (line.role !== 'system') continue
|
|
4019
|
+
const parsed = parseProposalOutcomeLine(line.content ?? '')
|
|
4020
|
+
if (parsed) outcomes.set(parsed.key, parsed)
|
|
4021
|
+
}
|
|
4022
|
+
return outcomes
|
|
4023
|
+
}
|
|
4024
|
+
|
|
4025
|
+
/** 경과 시간을 보이는 동안에만 돈다 — 답이 오면 멈춘다. */
|
|
4026
|
+
private _startElapsedTicker() {
|
|
4027
|
+
if (this.elapsedTimer) return
|
|
4028
|
+
this.elapsedTimer = setInterval(() => {
|
|
4029
|
+
const outcomes = this.proposalOutcomes
|
|
4030
|
+
const waiting = [...this.proposalSentAt.keys()].some(key => !outcomes.has(key))
|
|
4031
|
+
if (!waiting) {
|
|
4032
|
+
clearInterval(this.elapsedTimer)
|
|
4033
|
+
this.elapsedTimer = undefined
|
|
4034
|
+
return
|
|
4035
|
+
}
|
|
4036
|
+
this.requestUpdate()
|
|
4037
|
+
}, 1000)
|
|
4038
|
+
}
|
|
4039
|
+
|
|
3445
4040
|
/**
|
|
3446
4041
|
* 접지 경고 — 답이 언급했지만 **근거에 없던** 대상을 알린다.
|
|
3447
4042
|
*
|
|
@@ -3569,12 +4164,12 @@ export class OxAssistantChat extends LitElement {
|
|
|
3569
4164
|
/** 배지 문구 — 코드가 아니라 사람 말로. */
|
|
3570
4165
|
private outcomeLabel(outcome: string): string {
|
|
3571
4166
|
const map: Record<string, [string, string]> = {
|
|
3572
|
-
ok: ['ai-assistant.text.outcome-ok', '
|
|
3573
|
-
rejected: ['ai-assistant.text.outcome-rejected', '
|
|
3574
|
-
queued: ['ai-assistant.text.outcome-queued', '
|
|
3575
|
-
proposed: ['ai-assistant.text.outcome-proposed', '
|
|
3576
|
-
folded: ['ai-assistant.text.outcome-folded', '
|
|
3577
|
-
error: ['ai-assistant.text.outcome-error', '
|
|
4167
|
+
ok: ['ai-assistant.text.outcome-ok', 'read'],
|
|
4168
|
+
rejected: ['ai-assistant.text.outcome-rejected', 'rejected'],
|
|
4169
|
+
queued: ['ai-assistant.text.outcome-queued', 'to apply'],
|
|
4170
|
+
proposed: ['ai-assistant.text.outcome-proposed', 'proposed'],
|
|
4171
|
+
folded: ['ai-assistant.text.outcome-folded', 'duplicate'],
|
|
4172
|
+
error: ['ai-assistant.text.outcome-error', 'error']
|
|
3578
4173
|
}
|
|
3579
4174
|
const [key, fallback] = map[outcome] ?? map.ok
|
|
3580
4175
|
return i18next.t(key, { defaultValue: fallback })
|
|
@@ -3719,8 +4314,8 @@ export class OxAssistantChat extends LitElement {
|
|
|
3719
4314
|
|
|
3720
4315
|
const promptText = text || (attachmentsToSend.length > 0
|
|
3721
4316
|
? (attachmentsToSend[0].type.startsWith('image/')
|
|
3722
|
-
? '
|
|
3723
|
-
: '
|
|
4317
|
+
? i18next.t('ai-assistant.text.attached-image-ask', { defaultValue: 'here is an image — go by it.' })
|
|
4318
|
+
: i18next.t('ai-assistant.text.attached-file-ask', { defaultValue: 'here is a file — take a look.' }))
|
|
3724
4319
|
: '')
|
|
3725
4320
|
if (!this.sessionId && this.prepareSession) {
|
|
3726
4321
|
this.busy = true
|
|
@@ -3740,7 +4335,7 @@ export class OxAssistantChat extends LitElement {
|
|
|
3740
4335
|
let requestBase: any
|
|
3741
4336
|
try {
|
|
3742
4337
|
const provided = this.contextProvider?.() || {}
|
|
3743
|
-
requestContext = structuredClone({
|
|
4338
|
+
requestContext = structuredClone({ hostContext: this.conversation.hostContext, ...provided })
|
|
3744
4339
|
liveBoard = structuredClone(this.boardProvider ? this.boardProvider() : this.currentBoard)
|
|
3745
4340
|
requestBase = { version: 1, sessionId: this.sessionId, hostContext: requestContext.hostContext,
|
|
3746
4341
|
modelFingerprint: assistantModelFingerprint(liveBoard) }
|
|
@@ -3825,11 +4420,9 @@ export class OxAssistantChat extends LitElement {
|
|
|
3825
4420
|
? buildAssistantChatInput({
|
|
3826
4421
|
sessionId: this.sessionId,
|
|
3827
4422
|
history,
|
|
3828
|
-
|
|
4423
|
+
mode: { kind: this.conversation.kind!, target: this.conversation.target },
|
|
3829
4424
|
truncateAfterMessageId: this._truncateAfterId,
|
|
3830
|
-
hostContext: requestContext.hostContext
|
|
3831
|
-
toolCategories: this.conversation.toolCategories,
|
|
3832
|
-
requireGroundingTools: this.requireGroundingTools
|
|
4425
|
+
hostContext: requestContext.hostContext
|
|
3833
4426
|
})
|
|
3834
4427
|
: buildChatMutationInput({
|
|
3835
4428
|
sessionId: this.sessionId,
|
|
@@ -3838,7 +4431,8 @@ export class OxAssistantChat extends LitElement {
|
|
|
3838
4431
|
scopes: this.scopes,
|
|
3839
4432
|
truncateAfterMessageId: this._truncateAfterId,
|
|
3840
4433
|
hostContext: requestContext.hostContext,
|
|
3841
|
-
|
|
4434
|
+
/* The board door still takes its categories from the element — it declares no mode. */
|
|
4435
|
+
toolCategories: this.toolCategories,
|
|
3842
4436
|
boardTools: this.boardTools,
|
|
3843
4437
|
requireGroundingTools: this.requireGroundingTools,
|
|
3844
4438
|
knownTypes: this.knownTypes,
|