@things-factory/ai-assistant 10.1.6 → 10.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/client/components/board-ai-chat.ts +165 -100
  2. package/client/components/chat-defaults.ts +13 -0
  3. package/client/components/chat-input-builder.test.ts +81 -0
  4. package/client/components/chat-input-builder.ts +43 -0
  5. package/client/components/chat-triggers.test.ts +72 -0
  6. package/client/components/chat-triggers.ts +62 -0
  7. package/dist-client/components/board-ai-chat.d.ts +8 -0
  8. package/dist-client/components/board-ai-chat.js +160 -100
  9. package/dist-client/components/board-ai-chat.js.map +1 -1
  10. package/dist-client/components/chat-defaults.d.ts +12 -0
  11. package/dist-client/components/chat-defaults.js +2 -0
  12. package/dist-client/components/chat-defaults.js.map +1 -1
  13. package/dist-client/components/chat-input-builder.d.ts +29 -0
  14. package/dist-client/components/chat-input-builder.js +22 -0
  15. package/dist-client/components/chat-input-builder.js.map +1 -1
  16. package/dist-client/components/chat-input-builder.test.d.ts +1 -0
  17. package/dist-client/components/chat-input-builder.test.js +67 -0
  18. package/dist-client/components/chat-input-builder.test.js.map +1 -0
  19. package/dist-client/components/chat-triggers.d.ts +16 -0
  20. package/dist-client/components/chat-triggers.js +42 -0
  21. package/dist-client/components/chat-triggers.js.map +1 -0
  22. package/dist-client/components/chat-triggers.test.d.ts +1 -0
  23. package/dist-client/components/chat-triggers.test.js +61 -0
  24. package/dist-client/components/chat-triggers.test.js.map +1 -0
  25. package/dist-client/tsconfig.tsbuildinfo +1 -1
  26. package/dist-server/service/assistant-chat-resolver.d.ts +32 -0
  27. package/dist-server/service/assistant-chat-resolver.js +342 -0
  28. package/dist-server/service/assistant-chat-resolver.js.map +1 -0
  29. package/dist-server/service/chat-session-resolver.js +18 -18
  30. package/dist-server/service/chat-session-resolver.js.map +1 -1
  31. package/dist-server/service/index.d.ts +3 -1
  32. package/dist-server/service/index.js +4 -0
  33. package/dist-server/service/index.js.map +1 -1
  34. package/dist-server/tsconfig.tsbuildinfo +1 -1
  35. package/package.json +5 -5
  36. package/server/service/assistant-chat-resolver.ts +326 -0
  37. package/server/service/chat-session-resolver.ts +18 -18
  38. package/server/service/index.ts +4 -0
  39. package/test/translations.test.ts +79 -0
  40. package/translations/en.json +59 -0
  41. package/translations/ja.json +58 -0
  42. package/translations/ko.json +59 -0
  43. package/translations/ms.json +58 -0
  44. package/translations/zh.json +58 -0
@@ -25,7 +25,7 @@ import { i18next } from '@operato/i18n';
25
25
  import { ScrollbarStyles } from '@operato/styles';
26
26
  import gql from 'graphql-tag';
27
27
  import { renderMarkdown, injectMentionRefids, injectMentionUserIds, stripMentionRefids } from './markdown.js';
28
- import { buildChatMutationInput } from './chat-input-builder.js';
28
+ import { buildAssistantChatInput, buildChatMutationInput } from './chat-input-builder.js';
29
29
  import { receiveRemoteMessage, reconcileSentLine, remoteMessageToLine } from './chat-echo-dedup.js';
30
30
  import './mention-popup.js';
31
31
  import { buildMentionCandidates, buildCatalogCandidates, buildSlashCandidates, buildUserCandidates, extractMentionContext, filterMentionCandidates, applyMentionSelection, resolveMentionToken } from './mention-popup.js';
@@ -41,6 +41,7 @@ import { buildMentionCandidates, buildCatalogCandidates, buildSlashCandidates, b
41
41
  * 뜨지 않는 것이 맞다.
42
42
  */
43
43
  import { chatDefaults } from './chat-defaults.js';
44
+ import { availableTriggers } from './chat-triggers.js';
44
45
  const BOARD_AI_CHAT_MUTATION = gql `
45
46
  mutation BoardAIChat($input: BoardAIChatInput!) {
46
47
  boardAIChat(input: $input) {
@@ -60,6 +61,29 @@ const BOARD_AI_CHAT_MUTATION = gql `
60
61
  }
61
62
  }
62
63
  `;
64
+ /*
65
+ * The neutral door. Selected by `chatEndpoint`, and it asks for fewer fields because there is
66
+ * less to ask for: no patch, no scene actions, no patch id — this surface has no document.
67
+ *
68
+ * `offeredTools` has no counterpart on the board side. It is what the server actually put in
69
+ * front of the model this turn, so a surface that granted a category and sees it missing knows
70
+ * the registration never happened, instead of reading a vague answer as a refusal.
71
+ */
72
+ const ASSISTANT_CHAT_MUTATION = gql `
73
+ mutation AssistantChat($input: AssistantChatInput!) {
74
+ assistantChat(input: $input) {
75
+ reply
76
+ clientId
77
+ sessionId
78
+ userMessageId
79
+ assistantMessageId
80
+ toolUsages
81
+ offeredTools
82
+ proposals
83
+ groundingWarnings
84
+ }
85
+ }
86
+ `;
63
87
  const CHAT_MESSAGES_QUERY = gql `
64
88
  query ChatMessages($sessionId: String!, $limit: Int, $offset: Int) {
65
89
  chatMessages(sessionId: $sessionId, limit: $limit, offset: $offset) {
@@ -151,6 +175,22 @@ let OxBoardAIChat = class OxBoardAIChat extends LitElement {
151
175
  this.selectedRefids = [];
152
176
  /** 코어 보드 편집 도구 사용 여부. 기본 true. 보드를 고칠 이유가 없는 면에서 false. */
153
177
  this.boardTools = true;
178
+ /*
179
+ * Which server operation this surface talks to. A closed set of two, named after the operations
180
+ * themselves so a reader can grep either way.
181
+ *
182
+ * boardAIChat board authoring — lives in @things-factory/board-ai, carries the board model,
183
+ * returns patches and scene actions
184
+ * assistantChat a surface with no document of its own — lives here, runs registered tools only
185
+ *
186
+ * Default stays `boardAIChat`: every existing consumer is a board surface, and a default that
187
+ * changed under them would move their conversation to a door with no board tools behind it.
188
+ *
189
+ * A host that has no board must pass `assistantChat` **and** `systemPrompt`. Until it does, the
190
+ * chat calls a mutation that lives in a package it may not even load — which is how
191
+ * operato-figure got `Cannot query field "boardAIChat" on type "Mutation"` (2026-09-11).
192
+ */
193
+ this.chatEndpoint = 'boardAIChat';
154
194
  /**
155
195
  * 라이브 상태를 다루는 대화면인가 — true 면 **첫 턴에 도구 호출을 강제**한다.
156
196
  *
@@ -235,7 +275,7 @@ let OxBoardAIChat = class OxBoardAIChat extends LitElement {
235
275
  ]);
236
276
  }
237
277
  else {
238
- this.showToast(i18next.t('board-ai.text.mention-not-found', {
278
+ this.showToast(i18next.t('ai-assistant.text.mention-not-found', {
239
279
  token,
240
280
  defaultValue: `#${token} 을(를) 찾을 수 없습니다`
241
281
  }));
@@ -1723,7 +1763,7 @@ let OxBoardAIChat = class OxBoardAIChat extends LitElement {
1723
1763
  this._seenMessageIds = new Set(msgs.map((m) => m.id).filter(Boolean));
1724
1764
  }
1725
1765
  catch (e) {
1726
- this.errorMessage = `${i18next.t('board-ai.text.failed-to-load-history')}: ${e.message ?? e}`;
1766
+ this.errorMessage = `${i18next.t('ai-assistant.text.failed-to-load-history')}: ${e.message ?? e}`;
1727
1767
  }
1728
1768
  }
1729
1769
  render() {
@@ -1753,9 +1793,9 @@ let OxBoardAIChat = class OxBoardAIChat extends LitElement {
1753
1793
  const showActions = line.role !== 'system' && !line.pending && content.length > 0;
1754
1794
  return html `
1755
1795
  ${showLabel
1756
- ? html `<div class="msg-label">${i18next.t('board-ai.label.ai')}</div>`
1796
+ ? html `<div class="msg-label">${i18next.t('ai-assistant.label.ai')}</div>`
1757
1797
  : showSenderLabel
1758
- ? html `<div class="msg-label sender">${line.senderName || i18next.t('board-ai.label.participant')}</div>`
1798
+ ? html `<div class="msg-label sender">${line.senderName || i18next.t('ai-assistant.label.participant')}</div>`
1759
1799
  : nothing}
1760
1800
  <div class="msg-wrap ${line.role} ${isOther ? 'other' : ''}">
1761
1801
  <div
@@ -1769,7 +1809,7 @@ let OxBoardAIChat = class OxBoardAIChat extends LitElement {
1769
1809
  : line.role === 'system'
1770
1810
  ? html `<span
1771
1811
  class="sys-text"
1772
- title=${i18next.t('board-ai.text.toggle-system-note', { defaultValue: '눌러서 펼치기' })}
1812
+ title=${i18next.t('ai-assistant.text.toggle-system-note', { defaultValue: '눌러서 펼치기' })}
1773
1813
  @click=${() => this.toggleSystemNote(idx)}
1774
1814
  >${content}</span
1775
1815
  >`
@@ -1780,14 +1820,14 @@ let OxBoardAIChat = class OxBoardAIChat extends LitElement {
1780
1820
  <md-icon>check_circle</md-icon>
1781
1821
  <span class="summary-text">
1782
1822
  ${this.revertedPatchIds.has(line.patchId)
1783
- ? i18next.t('board-ai.text.reverted')
1784
- : i18next.t('board-ai.text.applied-changes')}
1823
+ ? i18next.t('ai-assistant.text.reverted')
1824
+ : i18next.t('ai-assistant.text.applied-changes')}
1785
1825
  </span>
1786
1826
  <button
1787
1827
  class="revert-btn"
1788
1828
  ?disabled=${this.revertedPatchIds.has(line.patchId)}
1789
1829
  @click=${() => line.patchId && this.onRevertClick(line.patchId)}>
1790
- ${i18next.t('board-ai.button.revert')}
1830
+ ${i18next.t('ai-assistant.button.revert')}
1791
1831
  </button>
1792
1832
  </div>
1793
1833
  `
@@ -1816,7 +1856,7 @@ let OxBoardAIChat = class OxBoardAIChat extends LitElement {
1816
1856
  ${this.lastFailedInput
1817
1857
  ? html `
1818
1858
  <button class="retry-btn" @click=${this.retryLastSend}>
1819
- ${i18next.t('board-ai.button.retry')}
1859
+ ${i18next.t('ai-assistant.button.retry')}
1820
1860
  </button>
1821
1861
  `
1822
1862
  : nothing}
@@ -1831,11 +1871,11 @@ let OxBoardAIChat = class OxBoardAIChat extends LitElement {
1831
1871
  <button
1832
1872
  class="action"
1833
1873
  @click=${() => (this.examplesOpen = !this.examplesOpen)}
1834
- title=${i18next.t('board-ai.text.show-examples-tooltip')}>
1874
+ title=${i18next.t('ai-assistant.text.show-examples-tooltip')}>
1835
1875
  <md-icon>${this.examplesOpen ? 'close' : 'lightbulb'}</md-icon>
1836
1876
  ${this.examplesOpen
1837
- ? i18next.t('board-ai.button.close')
1838
- : i18next.t('board-ai.button.examples')}
1877
+ ? i18next.t('ai-assistant.button.close')
1878
+ : i18next.t('ai-assistant.button.examples')}
1839
1879
  </button>
1840
1880
  </div>
1841
1881
  ${this.examplesOpen ? this.renderInlineExamples() : nothing}
@@ -1854,7 +1894,7 @@ let OxBoardAIChat = class OxBoardAIChat extends LitElement {
1854
1894
  }}
1855
1895
  ?disabled=${this.busy}
1856
1896
  rows="1"
1857
- placeholder=${this.placeholder ?? i18next.t('board-ai.text.input-placeholder')}></textarea>
1897
+ placeholder=${this.placeholder ?? i18next.t('ai-assistant.text.input-placeholder')}></textarea>
1858
1898
  ${this.mentionOpen
1859
1899
  ? html `
1860
1900
  <ox-mention-popup
@@ -1877,7 +1917,7 @@ let OxBoardAIChat = class OxBoardAIChat extends LitElement {
1877
1917
  <button
1878
1918
  ?disabled=${this.busy || !this.input.trim()}
1879
1919
  @click=${this.send}
1880
- title=${i18next.t('board-ai.text.send-tooltip')}>
1920
+ title=${i18next.t('ai-assistant.text.send-tooltip')}>
1881
1921
  ${this.busy
1882
1922
  ? html `<span class="spinner"></span>`
1883
1923
  : html `<md-icon>arrow_upward</md-icon>`}
@@ -2155,26 +2195,34 @@ let OxBoardAIChat = class OxBoardAIChat extends LitElement {
2155
2195
  items: group.itemKeys.map(key => i18next.t(key))
2156
2196
  }));
2157
2197
  }
2198
+ /** 이 표면이 실제로 쓸 수 있는 트리거 — 판정은 `chat-triggers.ts` 가 하고 여기서는 사실만 읽는다. */
2199
+ get triggerChips() {
2200
+ return availableTriggers({
2201
+ hasMentionableThings: !!(this.boardProvider || this.currentBoard || this.knownTypes?.length),
2202
+ hasSession: !!this.sessionId,
2203
+ slashTemplateCount: chatDefaults.slashTemplates.length
2204
+ });
2205
+ }
2158
2206
  static { this.EXAMPLE_GROUPS = [
2159
2207
  {
2160
- labelKey: 'board-ai.label.create',
2208
+ labelKey: 'ai-assistant.label.create',
2161
2209
  itemKeys: [
2162
- 'board-ai.example.create-monitoring-dashboard',
2163
- 'board-ai.example.create-welcome-screen'
2210
+ 'ai-assistant.example.create-monitoring-dashboard',
2211
+ 'ai-assistant.example.create-welcome-screen'
2164
2212
  ]
2165
2213
  },
2166
2214
  {
2167
- labelKey: 'board-ai.label.edit',
2215
+ labelKey: 'ai-assistant.label.edit',
2168
2216
  itemKeys: [
2169
- 'board-ai.example.edit-align-distribute',
2170
- 'board-ai.example.edit-resize-board'
2217
+ 'ai-assistant.example.edit-align-distribute',
2218
+ 'ai-assistant.example.edit-resize-board'
2171
2219
  ]
2172
2220
  },
2173
2221
  {
2174
- labelKey: 'board-ai.label.style',
2222
+ labelKey: 'ai-assistant.label.style',
2175
2223
  itemKeys: [
2176
- 'board-ai.example.style-dark-mode',
2177
- 'board-ai.example.style-rounded-shadow'
2224
+ 'ai-assistant.example.style-dark-mode',
2225
+ 'ai-assistant.example.style-rounded-shadow'
2178
2226
  ]
2179
2227
  }
2180
2228
  ]; }
@@ -2187,7 +2235,7 @@ let OxBoardAIChat = class OxBoardAIChat extends LitElement {
2187
2235
  <div class="session-tabs" role="tablist">
2188
2236
  ${this.sessions.map((s, i) => {
2189
2237
  const isActive = s.id === this.sessionId;
2190
- const fallback = i18next.t('board-ai.text.session-tab-label', {
2238
+ const fallback = i18next.t('ai-assistant.text.session-tab-label', {
2191
2239
  n: i + 1,
2192
2240
  defaultValue: `세션 ${i + 1}`
2193
2241
  });
@@ -2208,12 +2256,12 @@ let OxBoardAIChat = class OxBoardAIChat extends LitElement {
2208
2256
  })}
2209
2257
  <button
2210
2258
  class="session-tab-new"
2211
- title=${i18next.t('board-ai.button.new-chat', {
2259
+ title=${i18next.t('ai-assistant.button.new-chat', {
2212
2260
  defaultValue: '새 대화'
2213
2261
  })}
2214
2262
  @click=${this._onSessionCreateClick}>
2215
2263
  <md-icon>add</md-icon>
2216
- <span>${i18next.t('board-ai.button.new-chat', { defaultValue: '새 대화' })}</span>
2264
+ <span>${i18next.t('ai-assistant.button.new-chat', { defaultValue: '새 대화' })}</span>
2217
2265
  </button>
2218
2266
  </div>
2219
2267
  `;
@@ -2254,36 +2302,28 @@ let OxBoardAIChat = class OxBoardAIChat extends LitElement {
2254
2302
  <div class="empty">
2255
2303
  <div class="header">
2256
2304
  <md-icon class="icon">auto_awesome</md-icon>
2257
- <div class="title">${this.intro?.title ?? i18next.t('board-ai.text.empty-title')}</div>
2258
- <div class="subtitle">${this.intro?.subtitle ?? i18next.t('board-ai.text.empty-subtitle')}</div>
2305
+ <div class="title">${this.intro?.title ?? i18next.t('ai-assistant.text.empty-title')}</div>
2306
+ <div class="subtitle">${this.intro?.subtitle ?? i18next.t('ai-assistant.text.empty-subtitle')}</div>
2259
2307
  </div>
2260
2308
 
2261
- <div class="trigger-row">
2262
- <div class="trigger-row-label">${i18next.t('board-ai.text.triggers-heading')}</div>
2263
- <div class="trigger-chips">
2264
- <button
2265
- class="trigger-chip"
2266
- title=${i18next.t('board-ai.text.trigger-component-tooltip')}
2267
- @click=${() => this.insertTrigger('#')}>
2268
- <span class="trigger-char">#</span>
2269
- <span class="trigger-desc">${i18next.t('board-ai.label.trigger-component')}</span>
2270
- </button>
2271
- <button
2272
- class="trigger-chip"
2273
- title=${i18next.t('board-ai.text.trigger-user-tooltip')}
2274
- @click=${() => this.insertTrigger('@')}>
2275
- <span class="trigger-char">@</span>
2276
- <span class="trigger-desc">${i18next.t('board-ai.label.trigger-user')}</span>
2277
- </button>
2278
- <button
2279
- class="trigger-chip"
2280
- title=${i18next.t('board-ai.text.trigger-insert-tooltip')}
2281
- @click=${() => this.insertTrigger('/')}>
2282
- <span class="trigger-char">/</span>
2283
- <span class="trigger-desc">${i18next.t('board-ai.label.trigger-insert')}</span>
2284
- </button>
2285
- </div>
2286
- </div>
2309
+ ${this.triggerChips.length > 0
2310
+ ? html `
2311
+ <div class="trigger-row">
2312
+ <div class="trigger-row-label">${i18next.t('ai-assistant.text.triggers-heading')}</div>
2313
+ <div class="trigger-chips">
2314
+ ${this.triggerChips.map(trigger => html `
2315
+ <button
2316
+ class="trigger-chip"
2317
+ title=${i18next.t(trigger.tooltipKey)}
2318
+ @click=${() => this.insertTrigger(trigger.char)}>
2319
+ <span class="trigger-char">${trigger.char}</span>
2320
+ <span class="trigger-desc">${i18next.t(trigger.labelKey)}</span>
2321
+ </button>
2322
+ `)}
2323
+ </div>
2324
+ </div>
2325
+ `
2326
+ : ''}
2287
2327
 
2288
2328
  ${this.exampleGroups.map(group => html `
2289
2329
  <div class="group">
@@ -2297,11 +2337,12 @@ let OxBoardAIChat = class OxBoardAIChat extends LitElement {
2297
2337
  `)}
2298
2338
 
2299
2339
  <div class="footer">
2300
- <span class="badge">${i18next.t('board-ai.label.korean-supported')}</span>
2301
- <span class="badge">${i18next.t('board-ai.label.multi-command')}</span>
2302
- <span class="badge">${i18next.t('board-ai.label.review-able')}</span>
2303
- <br />
2304
- ${i18next.t('board-ai.text.empty-footer-notice')}
2340
+ <span class="badge">${i18next.t('ai-assistant.label.korean-supported')}</span>
2341
+ <span class="badge">${i18next.t('ai-assistant.label.multi-command')}</span>
2342
+ <span class="badge">${i18next.t('ai-assistant.label.review-able')}</span>
2343
+ ${chatDefaults.footerNoticeKey
2344
+ ? html `<br />${i18next.t(chatDefaults.footerNoticeKey)}`
2345
+ : ''}
2305
2346
  </div>
2306
2347
  </div>
2307
2348
  `;
@@ -2370,15 +2411,15 @@ let OxBoardAIChat = class OxBoardAIChat extends LitElement {
2370
2411
  <button
2371
2412
  class="msg-action ${isCopied ? 'confirmed' : ''}"
2372
2413
  title=${isCopied
2373
- ? i18next.t('board-ai.text.copied')
2374
- : i18next.t('board-ai.button.copy')}
2414
+ ? i18next.t('ai-assistant.text.copied')
2415
+ : i18next.t('ai-assistant.button.copy')}
2375
2416
  @click=${() => this.copyMessage(idx, content)}>
2376
2417
  <md-icon>${isCopied ? 'check' : 'content_copy'}</md-icon>
2377
2418
  </button>
2378
2419
  <button
2379
2420
  class="msg-action"
2380
2421
  ?disabled=${this.busy}
2381
- title=${i18next.t('board-ai.button.regenerate')}
2422
+ title=${i18next.t('ai-assistant.button.regenerate')}
2382
2423
  @click=${() => this.regenerateAssistant(idx)}>
2383
2424
  <md-icon>refresh</md-icon>
2384
2425
  </button>
@@ -2403,19 +2444,19 @@ let OxBoardAIChat = class OxBoardAIChat extends LitElement {
2403
2444
  <md-icon>unfold_less</md-icon>
2404
2445
  <span class="fn-text">
2405
2446
  ${folded.summarized
2406
- ? i18next.t('board-ai.text.history-folded-summarized', {
2447
+ ? i18next.t('ai-assistant.text.history-folded-summarized', {
2407
2448
  count: folded.omitted,
2408
2449
  defaultValue: '대화가 길어져 앞부분 {count}개가 요약으로 접혔습니다. 다른 주제라면 새 대화가 낫습니다.'
2409
2450
  })
2410
- : i18next.t('board-ai.text.history-folded', {
2451
+ : i18next.t('ai-assistant.text.history-folded', {
2411
2452
  count: folded.omitted,
2412
2453
  defaultValue: '대화가 길어져 앞부분 {count}개가 생략되었습니다. 다른 주제라면 새 대화가 낫습니다.'
2413
2454
  })}
2414
2455
  </span>
2415
2456
  <button class="fn-new" @click=${this.startNewFromNotice}>
2416
- ${i18next.t('board-ai.button.new-chat')}
2457
+ ${i18next.t('ai-assistant.button.new-chat')}
2417
2458
  </button>
2418
- <button class="fn-close" title=${i18next.t('board-ai.button.close')} @click=${() => (this.foldNoticeDismissed = true)}>
2459
+ <button class="fn-close" title=${i18next.t('ai-assistant.button.close')} @click=${() => (this.foldNoticeDismissed = true)}>
2419
2460
  <md-icon>close</md-icon>
2420
2461
  </button>
2421
2462
  </div>
@@ -2448,8 +2489,8 @@ let OxBoardAIChat = class OxBoardAIChat extends LitElement {
2448
2489
  class="run"
2449
2490
  ?disabled=${sent}
2450
2491
  @click=${() => this.executeProposal(p, key)}>
2451
- ${sent ? i18next.t('board-ai.text.proposal-sent', { defaultValue: '실행 요청됨' })
2452
- : i18next.t('board-ai.button.run-proposal', { defaultValue: '실행' })}
2492
+ ${sent ? i18next.t('ai-assistant.text.proposal-sent', { defaultValue: '실행 요청됨' })
2493
+ : i18next.t('ai-assistant.button.run-proposal', { defaultValue: '실행' })}
2453
2494
  </button>
2454
2495
  </div>
2455
2496
  `;
@@ -2509,7 +2550,7 @@ let OxBoardAIChat = class OxBoardAIChat extends LitElement {
2509
2550
  <div class="grounding-warning" role="note">
2510
2551
  <md-icon>report</md-icon>
2511
2552
  <span>
2512
- ${i18next.t('board-ai.text.ungrounded-mention', {
2553
+ ${i18next.t('ai-assistant.text.ungrounded-mention', {
2513
2554
  defaultValue: '확인되지 않은 대상을 언급했습니다 — 실제 데이터에 없습니다:'
2514
2555
  })}
2515
2556
  <span class="ids">${ids.join(', ')}</span>
@@ -2531,7 +2572,7 @@ let OxBoardAIChat = class OxBoardAIChat extends LitElement {
2531
2572
  const open = !!line.toolUsagesOpen;
2532
2573
  const readCount = usages.filter(u => u.kind === 'read').length;
2533
2574
  const writeCount = usages.filter(u => u.kind === 'write').length;
2534
- const toolsLabel = i18next.t('board-ai.text.tools-used', { defaultValue: '도구 사용' });
2575
+ const toolsLabel = i18next.t('ai-assistant.text.tools-used', { defaultValue: '도구 사용' });
2535
2576
  /* 눈에 걸려야 하는 것을 머리줄에 올린다 — 거절·접힘이 있으면 펼치기 전에 보인다.
2536
2577
  * (이번 사고들이 전부 그것이었다: 인자 빠진 호출이 거절됐고, 중복 제안이 접혔다.) */
2537
2578
  const problems = usages.filter(u => u.outcome === 'rejected' || u.outcome === 'error').length;
@@ -2547,12 +2588,12 @@ let OxBoardAIChat = class OxBoardAIChat extends LitElement {
2547
2588
  <strong>${usages.length}</strong> ${toolsLabel}
2548
2589
  <span class="tool-usages-counts">
2549
2590
  · read ${readCount} · write ${writeCount}${problems
2550
- ? html ` · <span class="tu-flag bad">${i18next.t('board-ai.text.tool-rejected-count', {
2591
+ ? html ` · <span class="tu-flag bad">${i18next.t('ai-assistant.text.tool-rejected-count', {
2551
2592
  count: problems,
2552
2593
  defaultValue: '거절 {count}'
2553
2594
  })}</span>`
2554
2595
  : nothing}${folded
2555
- ? html ` · <span class="tu-flag">${i18next.t('board-ai.text.tool-folded-count', {
2596
+ ? html ` · <span class="tu-flag">${i18next.t('ai-assistant.text.tool-folded-count', {
2556
2597
  count: folded,
2557
2598
  defaultValue: '접힘 {count}'
2558
2599
  })}</span>`
@@ -2595,7 +2636,7 @@ let OxBoardAIChat = class OxBoardAIChat extends LitElement {
2595
2636
  return html `
2596
2637
  ${newTurn
2597
2638
  ? html `<li class="tu-turn">
2598
- ${i18next.t('board-ai.text.tool-turn', { n: (u.iter ?? 0) + 1, defaultValue: '{n}번째 판단' })}
2639
+ ${i18next.t('ai-assistant.text.tool-turn', { n: (u.iter ?? 0) + 1, defaultValue: '{n}번째 판단' })}
2599
2640
  </li>`
2600
2641
  : nothing}
2601
2642
  <li class="tool-usage-item kind-${u.kind} outcome-${outcome}">
@@ -2620,12 +2661,12 @@ let OxBoardAIChat = class OxBoardAIChat extends LitElement {
2620
2661
  /** 배지 문구 — 코드가 아니라 사람 말로. */
2621
2662
  outcomeLabel(outcome) {
2622
2663
  const map = {
2623
- ok: ['board-ai.text.outcome-ok', '조회'],
2624
- rejected: ['board-ai.text.outcome-rejected', '거절'],
2625
- queued: ['board-ai.text.outcome-queued', '적용 예정'],
2626
- proposed: ['board-ai.text.outcome-proposed', '제안'],
2627
- folded: ['board-ai.text.outcome-folded', '중복 접힘'],
2628
- error: ['board-ai.text.outcome-error', '오류']
2664
+ ok: ['ai-assistant.text.outcome-ok', '조회'],
2665
+ rejected: ['ai-assistant.text.outcome-rejected', '거절'],
2666
+ queued: ['ai-assistant.text.outcome-queued', '적용 예정'],
2667
+ proposed: ['ai-assistant.text.outcome-proposed', '제안'],
2668
+ folded: ['ai-assistant.text.outcome-folded', '중복 접힘'],
2669
+ error: ['ai-assistant.text.outcome-error', '오류']
2629
2670
  };
2630
2671
  const [key, fallback] = map[outcome] ?? map.ok;
2631
2672
  return i18next.t(key, { defaultValue: fallback });
@@ -2684,7 +2725,7 @@ let OxBoardAIChat = class OxBoardAIChat extends LitElement {
2684
2725
  // 클립보드에는 marker 제거된 깨끗한 텍스트
2685
2726
  await navigator.clipboard.writeText(stripMentionRefids(content));
2686
2727
  this.copiedIdx = idx;
2687
- this.showToast(i18next.t('board-ai.text.copied'));
2728
+ this.showToast(i18next.t('ai-assistant.text.copied'));
2688
2729
  setTimeout(() => {
2689
2730
  if (this.copiedIdx === idx)
2690
2731
  this.copiedIdx = undefined;
@@ -2791,30 +2832,41 @@ let OxBoardAIChat = class OxBoardAIChat extends LitElement {
2791
2832
  // boardProvider 가 있으면 send 시점에 그것을 pull, 없으면 정적 currentBoard.
2792
2833
  const liveBoard = this.boardProvider ? this.boardProvider() : this.currentBoard;
2793
2834
  // mentions 는 위에서 이미 계산해 user line 에 thread — 그대로 mutation 입력으로 재사용.
2835
+ const neutral = this.chatEndpoint === 'assistantChat';
2794
2836
  const result = await client.mutate({
2795
- mutation: BOARD_AI_CHAT_MUTATION,
2837
+ mutation: neutral ? ASSISTANT_CHAT_MUTATION : BOARD_AI_CHAT_MUTATION,
2796
2838
  variables: {
2797
- input: buildChatMutationInput({
2798
- sessionId: this.sessionId,
2799
- history,
2800
- liveBoard,
2801
- scopes: this.scopes,
2802
- truncateAfterMessageId: this._truncateAfterId,
2803
- hostContext: this.hostContext,
2804
- toolCategories: this.toolCategories,
2805
- boardTools: this.boardTools,
2806
- requireGroundingTools: this.requireGroundingTools,
2807
- knownTypes: this.knownTypes,
2808
- categories: this.categories,
2809
- componentSchemas: this.componentSchemas,
2810
- selectedRefids: this.selectedRefids ?? [],
2811
- mentions
2812
- })
2839
+ input: neutral
2840
+ ? buildAssistantChatInput({
2841
+ sessionId: this.sessionId,
2842
+ history,
2843
+ systemPrompt: this.systemPrompt,
2844
+ truncateAfterMessageId: this._truncateAfterId,
2845
+ hostContext: this.hostContext,
2846
+ toolCategories: this.toolCategories,
2847
+ requireGroundingTools: this.requireGroundingTools
2848
+ })
2849
+ : buildChatMutationInput({
2850
+ sessionId: this.sessionId,
2851
+ history,
2852
+ liveBoard,
2853
+ scopes: this.scopes,
2854
+ truncateAfterMessageId: this._truncateAfterId,
2855
+ hostContext: this.hostContext,
2856
+ toolCategories: this.toolCategories,
2857
+ boardTools: this.boardTools,
2858
+ requireGroundingTools: this.requireGroundingTools,
2859
+ knownTypes: this.knownTypes,
2860
+ categories: this.categories,
2861
+ componentSchemas: this.componentSchemas,
2862
+ selectedRefids: this.selectedRefids ?? [],
2863
+ mentions
2864
+ })
2813
2865
  }
2814
2866
  });
2815
- const out = result.data?.boardAIChat;
2867
+ const out = neutral ? result.data?.assistantChat : result.data?.boardAIChat;
2816
2868
  if (!out)
2817
- throw new Error(i18next.t('board-ai.text.empty-response'));
2869
+ throw new Error(i18next.t('ai-assistant.text.empty-response'));
2818
2870
  /* 대화가 길어졌다는 서버 보고 — 닫지 않았다면 안내를 띄운다. */
2819
2871
  this.historyFolded = out.historyFolded ?? undefined;
2820
2872
  // 전송 성공 — picks 정리 (다음 메시지부터는 새 picks 만)
@@ -2915,6 +2967,14 @@ __decorate([
2915
2967
  property({ type: Boolean, attribute: 'board-tools' }),
2916
2968
  __metadata("design:type", Object)
2917
2969
  ], OxBoardAIChat.prototype, "boardTools", void 0);
2970
+ __decorate([
2971
+ property({ type: String, attribute: 'chat-endpoint' }),
2972
+ __metadata("design:type", String)
2973
+ ], OxBoardAIChat.prototype, "chatEndpoint", void 0);
2974
+ __decorate([
2975
+ property({ type: String, attribute: 'system-prompt' }),
2976
+ __metadata("design:type", String)
2977
+ ], OxBoardAIChat.prototype, "systemPrompt", void 0);
2918
2978
  __decorate([
2919
2979
  property({ type: Boolean, attribute: 'require-grounding-tools' }),
2920
2980
  __metadata("design:type", Object)