@things-factory/ai-assistant 10.1.13 → 10.1.15

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 (27) hide show
  1. package/client/components/{board-ai-chat.ts → assistant-chat.ts} +547 -25
  2. package/client/components/chat-input-builder.ts +1 -1
  3. package/client/index.ts +2 -3
  4. package/dist-client/components/{board-ai-chat.d.ts → assistant-chat.d.ts} +24 -3
  5. package/dist-client/components/{board-ai-chat.js → assistant-chat.js} +557 -72
  6. package/dist-client/components/assistant-chat.js.map +1 -0
  7. package/dist-client/components/{board-ai-chat.test.js → assistant-chat.test.js} +1 -1
  8. package/dist-client/components/assistant-chat.test.js.map +1 -0
  9. package/dist-client/components/chat-input-builder.js +1 -1
  10. package/dist-client/components/chat-input-builder.js.map +1 -1
  11. package/dist-client/index.d.ts +2 -3
  12. package/dist-client/index.js +2 -3
  13. package/dist-client/index.js.map +1 -1
  14. package/dist-client/tsconfig.tsbuildinfo +1 -1
  15. package/dist-server/service/assistant-chat-resolver.js +12 -0
  16. package/dist-server/service/assistant-chat-resolver.js.map +1 -1
  17. package/dist-server/tsconfig.tsbuildinfo +1 -1
  18. package/package.json +2 -2
  19. package/server/service/assistant-chat-resolver.ts +13 -0
  20. package/test/assistant-request-context.test.ts +1 -1
  21. package/test/assistant-session-toolbar.test.ts +1 -1
  22. package/translations/en.json +6 -1
  23. package/translations/ko.json +6 -1
  24. package/dist-client/components/board-ai-chat.js.map +0 -1
  25. package/dist-client/components/board-ai-chat.test.js.map +0 -1
  26. /package/client/components/{board-ai-chat.test.ts → assistant-chat.test.ts} +0 -0
  27. /package/dist-client/components/{board-ai-chat.test.d.ts → assistant-chat.test.d.ts} +0 -0
@@ -1,7 +1,7 @@
1
- var OxBoardAIChat_1;
1
+ var OxAssistantChat_1;
2
2
  import { __decorate, __metadata } from "tslib";
3
3
  /**
4
- * <ox-board-ai-chat> — AI 주도 보드 모델링 채팅 컴포넌트 (Lit).
4
+ * <ox-assistant-chat> — AI 도우미 채팅 컴포넌트 (Lit).
5
5
  *
6
6
  * 입력:
7
7
  * - sessionId: 영속 ChatSession 식별자 (없으면 ad-hoc 모드, 메시지 영속 안 됨)
@@ -44,6 +44,13 @@ import { buildMentionCandidates, buildCatalogCandidates, buildSlashCandidates, b
44
44
  */
45
45
  import { chatDefaults } from './chat-defaults.js';
46
46
  import { availableTriggers } from './chat-triggers.js';
47
+ function formatFileSize(bytes) {
48
+ if (!bytes || bytes < 1024)
49
+ return `${bytes || 0} B`;
50
+ if (bytes < 1024 * 1024)
51
+ return `${(bytes / 1024).toFixed(1)} KB`;
52
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
53
+ }
47
54
  const BOARD_AI_CHAT_MUTATION = gql `
48
55
  mutation BoardAIChat($input: BoardAIChatInput!) {
49
56
  boardAIChat(input: $input) {
@@ -154,7 +161,7 @@ function initials(name) {
154
161
  return (parts[0][0] + parts[1][0]).toUpperCase();
155
162
  return /[a-zA-Z]/.test(n) ? n.slice(0, 2).toUpperCase() : n.slice(0, 1);
156
163
  }
157
- let OxBoardAIChat = class OxBoardAIChat extends LitElement {
164
+ let OxAssistantChat = class OxAssistantChat extends LitElement {
158
165
  constructor() {
159
166
  super(...arguments);
160
167
  this.adoptingSession = false;
@@ -206,6 +213,10 @@ let OxBoardAIChat = class OxBoardAIChat extends LitElement {
206
213
  this.input = '';
207
214
  /** 되돌려진 patch id 들 — 한 번 revert 한 patch 는 버튼 비활성 */
208
215
  this.revertedPatchIds = new Set();
216
+ /** 첨부된 파일/이미지 목록 */
217
+ this.attachments = [];
218
+ /** 드래그 오버 상태 (시각 피드백용) */
219
+ this.dragOver = false;
209
220
  /** mini action — 인라인 예시 토글 */
210
221
  this.examplesOpen = false;
211
222
  /** 원본 JSON 을 펼친 단계(라인:단계) — 목록은 한 줄 요약으로 훑고 필요한 것만 펼친다. */
@@ -298,10 +309,92 @@ let OxBoardAIChat = class OxBoardAIChat extends LitElement {
298
309
  */
299
310
  this.startNewFromNotice = () => {
300
311
  this.foldNoticeDismissed = true;
301
- this.dispatchEvent(new CustomEvent('board-ai-start-new-session', { bubbles: true, composed: true }));
312
+ this.dispatchEvent(new CustomEvent('assistant-start-new-session', { bubbles: true, composed: true }));
302
313
  };
303
314
  }
304
- static { OxBoardAIChat_1 = this; }
315
+ static { OxAssistantChat_1 = this; }
316
+ async onAttachFiles(files) {
317
+ if (!files || files.length === 0)
318
+ return;
319
+ const list = Array.from(files);
320
+ const newAttachments = [];
321
+ for (const file of list) {
322
+ const id = 'att-' + Math.random().toString(36).substring(2, 9);
323
+ const url = URL.createObjectURL(file);
324
+ let base64;
325
+ try {
326
+ base64 = await this.readFileAsBase64(file);
327
+ }
328
+ catch (err) {
329
+ console.warn('Failed to read file as base64', err);
330
+ }
331
+ newAttachments.push({
332
+ id,
333
+ file,
334
+ name: file.name,
335
+ size: file.size,
336
+ type: file.type || 'application/octet-stream',
337
+ url,
338
+ base64
339
+ });
340
+ }
341
+ this.attachments = [...this.attachments, ...newAttachments];
342
+ }
343
+ readFileAsBase64(file) {
344
+ return new Promise((resolve, reject) => {
345
+ const reader = new FileReader();
346
+ reader.onload = () => resolve(reader.result);
347
+ reader.onerror = reject;
348
+ reader.readAsDataURL(file);
349
+ });
350
+ }
351
+ removeAttachment(id) {
352
+ const target = this.attachments.find(a => a.id === id);
353
+ if (target?.url) {
354
+ URL.revokeObjectURL(target.url);
355
+ }
356
+ this.attachments = this.attachments.filter(a => a.id !== id);
357
+ }
358
+ onComposerPaste(e) {
359
+ const items = e.clipboardData?.items;
360
+ if (!items || items.length === 0)
361
+ return;
362
+ const files = [];
363
+ for (let i = 0; i < items.length; i++) {
364
+ const item = items[i];
365
+ if (item.kind === 'file') {
366
+ const file = item.getAsFile();
367
+ if (file)
368
+ files.push(file);
369
+ }
370
+ }
371
+ if (files.length > 0) {
372
+ this.onAttachFiles(files);
373
+ }
374
+ }
375
+ onComposerDragOver(e) {
376
+ e.preventDefault();
377
+ e.stopPropagation();
378
+ if (!this.dragOver)
379
+ this.dragOver = true;
380
+ }
381
+ onComposerDragLeave(e) {
382
+ e.preventDefault();
383
+ e.stopPropagation();
384
+ const currentTarget = e.currentTarget;
385
+ const relatedTarget = e.relatedTarget;
386
+ if (!currentTarget.contains(relatedTarget)) {
387
+ this.dragOver = false;
388
+ }
389
+ }
390
+ onComposerDrop(e) {
391
+ e.preventDefault();
392
+ e.stopPropagation();
393
+ this.dragOver = false;
394
+ if (e.dataTransfer?.files?.length) {
395
+ this.onAttachFiles(e.dataTransfer.files);
396
+ }
397
+ }
305
398
  /** 현재 로그인 사용자 email — 그룹챗에서 내 메시지 vs 타인 메시지 구분.
306
399
  * email 을 쓰는 이유: /auth/profile 이 항상 내려주는 로그인 식별자이고(비어 있지 않다),
307
400
  * 내부 관리키(User.id)를 클라이언트 판정에 의존하지 않기 위함. */
@@ -1092,6 +1185,63 @@ let OxBoardAIChat = class OxBoardAIChat extends LitElement {
1092
1185
  .proposal.sent {
1093
1186
  border-left-color: var(--md-sys-color-outline, #94a3b8);
1094
1187
  }
1188
+ .proposal .staged-badge {
1189
+ display: inline-flex;
1190
+ align-items: center;
1191
+ gap: 3px;
1192
+ padding: 3px 8px;
1193
+ border-radius: 999px;
1194
+ background: rgba(15, 118, 110, 0.1);
1195
+ color: var(--md-sys-color-primary, #0f766e);
1196
+ font-size: 11px;
1197
+ font-weight: 600;
1198
+ flex-shrink: 0;
1199
+ }
1200
+ .proposal .staged-badge md-icon {
1201
+ --md-icon-size: 13px;
1202
+ color: inherit;
1203
+ }
1204
+
1205
+ .proposal-choices {
1206
+ display: flex;
1207
+ flex-wrap: wrap;
1208
+ gap: 6px;
1209
+ margin-top: 6px;
1210
+ padding: 6px 8px;
1211
+ background: var(--md-sys-color-surface-container-lowest, #ffffff);
1212
+ border: 1px solid var(--md-sys-color-outline-variant, #e2e8f0);
1213
+ border-radius: 8px;
1214
+ }
1215
+ .proposal-choices .choice-chip {
1216
+ display: inline-flex;
1217
+ align-items: center;
1218
+ gap: 4px;
1219
+ padding: 4px 9px;
1220
+ border-radius: 999px;
1221
+ border: 1px solid var(--md-sys-color-outline-variant, #cbd5e1);
1222
+ background: var(--md-sys-color-surface-container-low, #f8fafc);
1223
+ color: var(--md-sys-color-on-surface, #1e293b);
1224
+ font-family: inherit;
1225
+ font-size: 11px;
1226
+ font-weight: 550;
1227
+ cursor: pointer;
1228
+ transition: all 0.14s ease-in-out;
1229
+ }
1230
+ .proposal-choices .choice-chip:hover {
1231
+ background: var(--md-sys-color-surface-container-high, #e2e8f0);
1232
+ border-color: var(--md-sys-color-primary, #0f766e);
1233
+ color: var(--md-sys-color-primary, #0f766e);
1234
+ }
1235
+ .proposal-choices .choice-chip.primary {
1236
+ background: var(--md-sys-color-primary-container, #ccfbf1);
1237
+ color: var(--md-sys-color-on-primary-container, #0f766e);
1238
+ border-color: var(--md-sys-color-primary, #0f766e);
1239
+ font-weight: 650;
1240
+ }
1241
+ .proposal-choices .choice-chip md-icon {
1242
+ --md-icon-size: 13px;
1243
+ color: inherit;
1244
+ }
1095
1245
 
1096
1246
  /* ── 접지 경고 — 근거에 없는 대상을 지목한 답 ───── */
1097
1247
  .grounding-warning {
@@ -1394,10 +1544,156 @@ let OxBoardAIChat = class OxBoardAIChat extends LitElement {
1394
1544
 
1395
1545
  /* ── Composer (actions + input 통합 영역) ──────────── */
1396
1546
  .composer {
1547
+ position: relative;
1397
1548
  border-top: 1px solid var(--md-sys-color-outline-variant, #f1f5f9);
1398
1549
  background: var(--md-sys-color-surface, #ffffff);
1399
1550
  display: flex;
1400
1551
  flex-direction: column;
1552
+ transition: background 0.15s, border-color 0.15s;
1553
+ }
1554
+ .composer.drag-over {
1555
+ background: var(--md-sys-color-surface-container, #f1f5f9);
1556
+ border-top-color: var(--md-sys-color-primary, #0284c7);
1557
+ }
1558
+ .drag-drop-overlay {
1559
+ position: absolute;
1560
+ top: 0;
1561
+ left: 0;
1562
+ right: 0;
1563
+ bottom: 0;
1564
+ background: rgba(255, 255, 255, 0.94);
1565
+ backdrop-filter: blur(2px);
1566
+ z-index: 10;
1567
+ display: flex;
1568
+ flex-direction: column;
1569
+ align-items: center;
1570
+ justify-content: center;
1571
+ gap: 6px;
1572
+ border: 2px dashed var(--md-sys-color-primary, #0284c7);
1573
+ border-radius: 4px;
1574
+ color: var(--md-sys-color-primary, #0284c7);
1575
+ font-size: 13px;
1576
+ font-weight: 500;
1577
+ pointer-events: none;
1578
+ }
1579
+ .drag-drop-overlay md-icon {
1580
+ --md-icon-size: 28px;
1581
+ }
1582
+ .attachment-strip {
1583
+ display: flex;
1584
+ flex-wrap: wrap;
1585
+ gap: 6px;
1586
+ padding: 6px 14px 2px;
1587
+ max-height: 120px;
1588
+ overflow-y: auto;
1589
+ }
1590
+ .attachment-chip {
1591
+ display: flex;
1592
+ align-items: center;
1593
+ gap: 6px;
1594
+ background: var(--md-sys-color-surface-container, #f1f5f9);
1595
+ border: 1px solid var(--md-sys-color-outline-variant, #e2e8f0);
1596
+ border-radius: 8px;
1597
+ padding: 3px 6px 3px 4px;
1598
+ max-width: 220px;
1599
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
1600
+ }
1601
+ .attachment-thumb {
1602
+ width: 28px;
1603
+ height: 28px;
1604
+ object-fit: cover;
1605
+ border-radius: 4px;
1606
+ flex: none;
1607
+ }
1608
+ .attachment-icon {
1609
+ --md-icon-size: 20px;
1610
+ color: var(--md-sys-color-primary, #0284c7);
1611
+ flex: none;
1612
+ }
1613
+ .attachment-meta {
1614
+ display: flex;
1615
+ flex-direction: column;
1616
+ min-width: 0;
1617
+ flex: 1;
1618
+ }
1619
+ .attachment-name {
1620
+ font-size: 11px;
1621
+ font-weight: 500;
1622
+ color: var(--md-sys-color-on-surface, #0f172a);
1623
+ overflow: hidden;
1624
+ text-overflow: ellipsis;
1625
+ white-space: nowrap;
1626
+ }
1627
+ .attachment-size {
1628
+ font-size: 9px;
1629
+ color: var(--md-sys-color-outline, #64748b);
1630
+ }
1631
+ .attachment-remove {
1632
+ all: unset;
1633
+ display: flex;
1634
+ align-items: center;
1635
+ justify-content: center;
1636
+ width: 18px;
1637
+ height: 18px;
1638
+ border-radius: 50%;
1639
+ cursor: pointer;
1640
+ color: var(--md-sys-color-outline, #64748b);
1641
+ transition: background 0.15s, color 0.15s;
1642
+ flex: none;
1643
+ }
1644
+ .attachment-remove:hover {
1645
+ background: var(--md-sys-color-surface-container-highest, #cbd5e1);
1646
+ color: var(--md-sys-color-error, #ef4444);
1647
+ }
1648
+ .attachment-remove md-icon {
1649
+ --md-icon-size: 14px;
1650
+ }
1651
+ .msg-attachments {
1652
+ display: flex;
1653
+ flex-wrap: wrap;
1654
+ gap: 8px;
1655
+ margin-top: 8px;
1656
+ }
1657
+ .msg-attachment-img-wrap {
1658
+ border-radius: 8px;
1659
+ overflow: hidden;
1660
+ border: 1px solid var(--md-sys-color-outline-variant, #e2e8f0);
1661
+ background: var(--md-sys-color-surface-container-lowest, #ffffff);
1662
+ max-width: 240px;
1663
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
1664
+ display: flex;
1665
+ flex-direction: column;
1666
+ }
1667
+ .msg-attachment-img-wrap img {
1668
+ max-width: 100%;
1669
+ max-height: 160px;
1670
+ object-fit: cover;
1671
+ display: block;
1672
+ }
1673
+ .msg-attachment-label {
1674
+ padding: 4px 8px;
1675
+ font-size: 10px;
1676
+ color: var(--md-sys-color-on-surface-variant, #64748b);
1677
+ background: var(--md-sys-color-surface-container, #f8fafc);
1678
+ overflow: hidden;
1679
+ text-overflow: ellipsis;
1680
+ white-space: nowrap;
1681
+ border-top: 1px solid var(--md-sys-color-outline-variant, #f1f5f9);
1682
+ }
1683
+ .msg-attachment-file-chip {
1684
+ display: flex;
1685
+ align-items: center;
1686
+ gap: 6px;
1687
+ padding: 6px 10px;
1688
+ border-radius: 8px;
1689
+ background: var(--md-sys-color-surface-container, #f8fafc);
1690
+ border: 1px solid var(--md-sys-color-outline-variant, #e2e8f0);
1691
+ font-size: 11px;
1692
+ color: var(--md-sys-color-on-surface, #0f172a);
1693
+ }
1694
+ .msg-attachment-file-chip md-icon {
1695
+ --md-icon-size: 18px;
1696
+ color: var(--md-sys-color-primary, #0284c7);
1401
1697
  }
1402
1698
 
1403
1699
  /* ── Mini action row (메시지 시작 후) ──────────────── */
@@ -1824,6 +2120,25 @@ let OxBoardAIChat = class OxBoardAIChat extends LitElement {
1824
2120
  >${content}</span
1825
2121
  >`
1826
2122
  : content}
2123
+ ${line.attachments && line.attachments.length > 0
2124
+ ? html `
2125
+ <div class="msg-attachments">
2126
+ ${line.attachments.map(att => att.type?.startsWith('image/') || att.url?.startsWith('data:image/') || att.base64
2127
+ ? html `
2128
+ <div class="msg-attachment-img-wrap" title=${att.name}>
2129
+ <img src=${att.url || att.base64} alt=${att.name} />
2130
+ <span class="msg-attachment-label">${att.name}</span>
2131
+ </div>
2132
+ `
2133
+ : html `
2134
+ <div class="msg-attachment-file-chip" title=${att.name}>
2135
+ <md-icon>description</md-icon>
2136
+ <span>${att.name}</span>
2137
+ </div>
2138
+ `)}
2139
+ </div>
2140
+ `
2141
+ : nothing}
1827
2142
  ${line.patchId
1828
2143
  ? html `
1829
2144
  <div class="summary ${this.revertedPatchIds.has(line.patchId) ? 'reverted' : ''}">
@@ -1874,11 +2189,47 @@ let OxBoardAIChat = class OxBoardAIChat extends LitElement {
1874
2189
  `
1875
2190
  : nothing}
1876
2191
  ${this.renderFoldNotice()}
1877
- <div class="composer ${hasMessages ? '' : 'no-actions'}">
1878
- ${hasMessages
2192
+ <div
2193
+ class="composer ${this.dragOver ? 'drag-over' : ''}"
2194
+ @dragover=${(e) => this.onComposerDragOver(e)}
2195
+ @dragleave=${(e) => this.onComposerDragLeave(e)}
2196
+ @drop=${(e) => this.onComposerDrop(e)}
2197
+ @paste=${(e) => this.onComposerPaste(e)}>
2198
+ ${this.dragOver
2199
+ ? html `
2200
+ <div class="drag-drop-overlay">
2201
+ <md-icon>cloud_upload</md-icon>
2202
+ <span>${i18next.t('ai-assistant.text.drop-files-here', { defaultValue: '여기에 이미지나 파일을 놓으세요' })}</span>
2203
+ </div>
2204
+ `
2205
+ : nothing}
2206
+ <div class="actions-row">
2207
+ <input
2208
+ id="chat-file-input"
2209
+ type="file"
2210
+ multiple
2211
+ accept="image/*,.json,.txt,.csv"
2212
+ style="display: none"
2213
+ @change=${(e) => {
2214
+ const input = e.target;
2215
+ if (input.files?.length) {
2216
+ this.onAttachFiles(input.files);
2217
+ input.value = '';
2218
+ }
2219
+ }}
2220
+ />
2221
+ <button
2222
+ type="button"
2223
+ class="action"
2224
+ @click=${() => this.renderRoot.querySelector('#chat-file-input')?.click()}
2225
+ title=${i18next.t('ai-assistant.tooltip.attach-file', { defaultValue: '이미지 또는 파일 첨부 (클립보드 붙여넣기·드래그앤드롭 지원)' })}>
2226
+ <md-icon>attach_file</md-icon>
2227
+ ${i18next.t('ai-assistant.button.attach', { defaultValue: '첨부' })}
2228
+ </button>
2229
+ ${hasMessages
1879
2230
  ? html `
1880
- <div class="actions-row">
1881
2231
  <button
2232
+ type="button"
1882
2233
  class="action"
1883
2234
  @click=${() => (this.examplesOpen = !this.examplesOpen)}
1884
2235
  title=${i18next.t('ai-assistant.text.show-examples-tooltip')}>
@@ -1887,8 +2238,32 @@ let OxBoardAIChat = class OxBoardAIChat extends LitElement {
1887
2238
  ? i18next.t('ai-assistant.button.close')
1888
2239
  : i18next.t('ai-assistant.button.examples')}
1889
2240
  </button>
2241
+ `
2242
+ : nothing}
2243
+ </div>
2244
+ ${this.examplesOpen ? this.renderInlineExamples() : nothing}
2245
+ ${this.attachments.length > 0
2246
+ ? html `
2247
+ <div class="attachment-strip">
2248
+ ${this.attachments.map(att => html `
2249
+ <div class="attachment-chip" title=${att.name}>
2250
+ ${att.type.startsWith('image/')
2251
+ ? html `<img class="attachment-thumb" src=${att.url} alt=${att.name} />`
2252
+ : html `<md-icon class="attachment-icon">description</md-icon>`}
2253
+ <div class="attachment-meta">
2254
+ <span class="attachment-name">${att.name}</span>
2255
+ <span class="attachment-size">${formatFileSize(att.size)}</span>
2256
+ </div>
2257
+ <button
2258
+ type="button"
2259
+ class="attachment-remove"
2260
+ title=${i18next.t('ai-assistant.button.remove-attachment', { defaultValue: '첨부 삭제' })}
2261
+ @click=${() => this.removeAttachment(att.id)}>
2262
+ <md-icon>close</md-icon>
2263
+ </button>
2264
+ </div>
2265
+ `)}
1890
2266
  </div>
1891
- ${this.examplesOpen ? this.renderInlineExamples() : nothing}
1892
2267
  `
1893
2268
  : nothing}
1894
2269
  <div class="input-row">
@@ -1925,7 +2300,7 @@ let OxBoardAIChat = class OxBoardAIChat extends LitElement {
1925
2300
  : nothing}
1926
2301
  </div>
1927
2302
  <button
1928
- ?disabled=${this.busy || !this.input.trim()}
2303
+ ?disabled=${this.busy || (!this.input.trim() && this.attachments.length === 0)}
1929
2304
  @click=${this.send}
1930
2305
  title=${i18next.t('ai-assistant.text.send-tooltip')}>
1931
2306
  ${this.busy
@@ -2200,7 +2575,7 @@ let OxBoardAIChat = class OxBoardAIChat extends LitElement {
2200
2575
  get exampleGroups() {
2201
2576
  if (this.examples?.length)
2202
2577
  return this.examples;
2203
- return OxBoardAIChat_1.EXAMPLE_GROUPS.map(group => ({
2578
+ return OxAssistantChat_1.EXAMPLE_GROUPS.map(group => ({
2204
2579
  label: i18next.t(group.labelKey),
2205
2580
  items: group.itemKeys.map(key => i18next.t(key))
2206
2581
  }));
@@ -2461,26 +2836,79 @@ let OxBoardAIChat = class OxBoardAIChat extends LitElement {
2461
2836
  ${items.map(p => {
2462
2837
  const key = this.proposalKey(p);
2463
2838
  const sent = this.sentProposals.has(key);
2839
+ const rawChoices = Array.isArray(p.choices) ? p.choices : (p.source ? [
2840
+ { id: 'save', action: 'save', label: i18next.t('label.save-as-draft', { defaultValue: '초안 저장' }), icon: 'save', primary: true },
2841
+ { id: 'repropose', action: 'prompt', label: '다른 콘셉트 재제안', icon: 'refresh', prompt: '현재 제안과 완전히 다른 스타일과 콘셉트로 새로 제안해줘' },
2842
+ { id: 'slim', action: 'prompt', label: '비율 슬림 조정', icon: 'aspect_ratio', prompt: '전체적인 가로세로 비율을 좀 더 슬림하고 날렵하게 조정해줘' },
2843
+ { id: 'palette', action: 'prompt', label: '색상 테마 변경', icon: 'palette', prompt: '다른 팔레트 색상 테마로 부품 색상을 변경해줘' },
2844
+ { id: 'detail', action: 'prompt', label: '디테일 보강', icon: 'extension', prompt: '주요 디테일과 센서 부품을 좀 더 풍부하게 보강해줘' }
2845
+ ] : []);
2464
2846
  return html `
2465
2847
  <div class="proposal ${sent ? 'sent' : ''}">
2466
- <md-icon>bolt</md-icon>
2848
+ <md-icon>${p.icon || (p.source ? 'view_in_ar' : 'bolt')}</md-icon>
2467
2849
  <span class="pbody">
2468
2850
  <span class="plabel">${p.label || p.command}</span>
2469
2851
  ${p.reason ? html `<span class="preason">${p.reason}</span>` : nothing}
2470
2852
  </span>
2471
- <button
2472
- class="run"
2473
- ?disabled=${sent}
2474
- @click=${() => this.executeProposal(p, key)}>
2475
- ${sent ? i18next.t('ai-assistant.text.proposal-sent', { defaultValue: '실행 요청됨' })
2853
+ ${!rawChoices.length ? html `
2854
+ <button
2855
+ class="run"
2856
+ ?disabled=${sent}
2857
+ @click=${() => this.executeProposal(p, key)}>
2858
+ ${sent ? i18next.t('ai-assistant.text.proposal-sent', { defaultValue: '실행 요청됨' })
2476
2859
  : i18next.t('ai-assistant.button.run-proposal', { defaultValue: '실행' })}
2477
- </button>
2860
+ </button>
2861
+ ` : html `
2862
+ <span class="staged-badge" title="3D 뷰어에 모델이 표시되었습니다">
2863
+ <md-icon>check_circle</md-icon>
2864
+ <span>뷰어 반영됨</span>
2865
+ </span>
2866
+ `}
2478
2867
  </div>
2868
+ ${rawChoices.length ? html `
2869
+ <div class="proposal-choices" role="group" aria-label="대화 선택지">
2870
+ ${rawChoices.map((c) => html `
2871
+ <button
2872
+ class="choice-chip ${c.primary ? 'primary' : ''}"
2873
+ title=${c.prompt || c.label}
2874
+ @click=${() => this.onChoiceClick(c, p, key)}>
2875
+ ${c.icon ? html `<md-icon>${c.icon}</md-icon>` : nothing}
2876
+ <span>${c.label}</span>
2877
+ </button>
2878
+ `)}
2879
+ </div>
2880
+ ` : nothing}
2479
2881
  `;
2480
2882
  })}
2481
2883
  </div>
2482
2884
  `;
2483
2885
  }
2886
+ onChoiceClick(choice, proposal, key) {
2887
+ if (choice.action === 'save') {
2888
+ this.dispatchEvent(new CustomEvent('assistant-choice', {
2889
+ detail: { action: 'save', choice, proposal, sessionId: this.sessionId },
2890
+ bubbles: true,
2891
+ composed: true
2892
+ }));
2893
+ this.executeProposal(proposal, key);
2894
+ return;
2895
+ }
2896
+ if (choice.action === 'prompt' && choice.prompt) {
2897
+ this.input = choice.prompt;
2898
+ this.dispatchEvent(new CustomEvent('assistant-choice', {
2899
+ detail: { action: 'prompt', choice, proposal, sessionId: this.sessionId },
2900
+ bubbles: true,
2901
+ composed: true
2902
+ }));
2903
+ void this.send();
2904
+ return;
2905
+ }
2906
+ this.dispatchEvent(new CustomEvent('assistant-choice', {
2907
+ detail: { action: choice.action || 'custom', choice, proposal, sessionId: this.sessionId },
2908
+ bubbles: true,
2909
+ composed: true
2910
+ }));
2911
+ }
2484
2912
  /**
2485
2913
  * 제안 식별 키 — 같은 조치를 두 번 실행하지 않도록 **효과**(명령·대상·인자)로만 만든다.
2486
2914
  *
@@ -2512,7 +2940,7 @@ let OxBoardAIChat = class OxBoardAIChat extends LitElement {
2512
2940
  */
2513
2941
  executeProposal(p, key) {
2514
2942
  this.sentProposals = new Set(this.sentProposals).add(key);
2515
- this.dispatchEvent(new CustomEvent('board-ai-proposal-execute', {
2943
+ this.dispatchEvent(new CustomEvent('assistant-proposal-execute', {
2516
2944
  detail: { proposal: p, sessionId: this.sessionId },
2517
2945
  bubbles: true,
2518
2946
  composed: true
@@ -2777,8 +3205,15 @@ let OxBoardAIChat = class OxBoardAIChat extends LitElement {
2777
3205
  }
2778
3206
  async send() {
2779
3207
  const text = this.input.trim();
2780
- if (!text || this.busy)
3208
+ const attachmentsToSend = [...this.attachments];
3209
+ if ((!text && attachmentsToSend.length === 0) || this.busy)
2781
3210
  return;
3211
+ this.attachments = [];
3212
+ const promptText = text || (attachmentsToSend.length > 0
3213
+ ? (attachmentsToSend[0].type.startsWith('image/')
3214
+ ? '첨부된 이미지를 참고하여 제안해 주세요.'
3215
+ : '첨부된 파일을 확인해 주세요.')
3216
+ : '');
2782
3217
  if (!this.sessionId && this.prepareSession) {
2783
3218
  this.busy = true;
2784
3219
  try {
@@ -2810,22 +3245,34 @@ let OxBoardAIChat = class OxBoardAIChat extends LitElement {
2810
3245
  // 등장하는 것만 추출 — 지운 토큰 제외. inline 마커로 content 에 주입.
2811
3246
  const mentions = [];
2812
3247
  for (const [token, refid] of this.pickedMentions.entries()) {
2813
- if (text.includes(`#${token}`))
3248
+ if (promptText.includes(`#${token}`))
2814
3249
  mentions.push({ token, refid });
2815
3250
  }
2816
3251
  const userMentions = [];
2817
3252
  for (const [token, userId] of this.pickedUserMentions.entries()) {
2818
- if (text.includes(`@${token}`))
3253
+ if (promptText.includes(`@${token}`))
2819
3254
  userMentions.push({ token, userId });
2820
3255
  }
2821
- let enrichedContent = injectMentionRefids(text, mentions);
3256
+ let enrichedContent = injectMentionRefids(promptText, mentions);
2822
3257
  enrichedContent = injectMentionUserIds(enrichedContent, userMentions);
2823
3258
  // optimistic 라인에 임시 핸들 — 방송 에코와의 reconcile 을 index 비의존으로.
2824
3259
  const userLocalId = `local-${++this._localSeq}-u`;
2825
3260
  const asstLocalId = `local-${++this._localSeq}-a`;
2826
3261
  this.lines = [
2827
3262
  ...this.lines,
2828
- { role: 'user', content: enrichedContent, _localId: userLocalId, senderEmail: this._myEmail }
3263
+ {
3264
+ role: 'user',
3265
+ content: enrichedContent,
3266
+ attachments: attachmentsToSend.map(a => ({
3267
+ name: a.name,
3268
+ size: a.size,
3269
+ type: a.type,
3270
+ url: a.url,
3271
+ base64: a.base64
3272
+ })),
3273
+ _localId: userLocalId,
3274
+ senderEmail: this._myEmail
3275
+ }
2829
3276
  ];
2830
3277
  this.input = '';
2831
3278
  this.busy = true;
@@ -2833,6 +3280,24 @@ let OxBoardAIChat = class OxBoardAIChat extends LitElement {
2833
3280
  // 가짜 pending 응답 표시 (UX 부드럽게)
2834
3281
  this.lines = [...this.lines, { role: 'assistant', content: '', pending: true, _localId: asstLocalId }];
2835
3282
  try {
3283
+ if (attachmentsToSend.length > 0) {
3284
+ requestContext.hostContext = {
3285
+ ...requestContext.hostContext,
3286
+ attachments: attachmentsToSend.map(a => ({
3287
+ name: a.name,
3288
+ size: a.size,
3289
+ mediaType: a.type,
3290
+ data: a.base64
3291
+ })),
3292
+ image: attachmentsToSend.find(a => a.type.startsWith('image/'))
3293
+ ? {
3294
+ name: attachmentsToSend.find(a => a.type.startsWith('image/')).name,
3295
+ mediaType: attachmentsToSend.find(a => a.type.startsWith('image/')).type,
3296
+ data: attachmentsToSend.find(a => a.type.startsWith('image/')).base64
3297
+ }
3298
+ : undefined
3299
+ };
3300
+ }
2836
3301
  // LLM 으로 보낼 history — user/assistant 만 (system 은 백엔드가 자동 합류).
2837
3302
  // pending placeholder 와 system 은 제외 (index 비의존 — 방송 append 와 무관).
2838
3303
  const history = this.lines
@@ -2895,6 +3360,18 @@ let OxBoardAIChat = class OxBoardAIChat extends LitElement {
2895
3360
  proposals: Array.isArray(out.proposals) ? out.proposals : undefined,
2896
3361
  pending: false
2897
3362
  });
3363
+ // 제안 자동 프리뷰 전파 (3D 뷰어 자동 반영)
3364
+ if (Array.isArray(out.proposals) && out.proposals.length > 0) {
3365
+ for (const p of out.proposals) {
3366
+ if (p?.source || p?.autoStage) {
3367
+ this.dispatchEvent(new CustomEvent('assistant-proposal-preview', {
3368
+ detail: { proposal: p, sessionId: out.sessionId },
3369
+ bubbles: true,
3370
+ composed: true
3371
+ }));
3372
+ }
3373
+ }
3374
+ }
2898
3375
  // 호스트로 patch 이벤트 전파
2899
3376
  if (out.patch) {
2900
3377
  this.dispatchEvent(new CustomEvent('board-edit-patch', {
@@ -2940,177 +3417,185 @@ let OxBoardAIChat = class OxBoardAIChat extends LitElement {
2940
3417
  __decorate([
2941
3418
  property({ type: String, attribute: 'session-id' }),
2942
3419
  __metadata("design:type", String)
2943
- ], OxBoardAIChat.prototype, "sessionId", void 0);
3420
+ ], OxAssistantChat.prototype, "sessionId", void 0);
2944
3421
  __decorate([
2945
3422
  property({ attribute: false }),
2946
3423
  __metadata("design:type", Function)
2947
- ], OxBoardAIChat.prototype, "prepareSession", void 0);
3424
+ ], OxAssistantChat.prototype, "prepareSession", void 0);
2948
3425
  __decorate([
2949
3426
  property({ type: Array, attribute: false }),
2950
3427
  __metadata("design:type", Array)
2951
- ], OxBoardAIChat.prototype, "sessions", void 0);
3428
+ ], OxAssistantChat.prototype, "sessions", void 0);
2952
3429
  __decorate([
2953
3430
  property({ type: Object }),
2954
3431
  __metadata("design:type", Object)
2955
- ], OxBoardAIChat.prototype, "currentBoard", void 0);
3432
+ ], OxAssistantChat.prototype, "currentBoard", void 0);
2956
3433
  __decorate([
2957
3434
  property({ attribute: false }),
2958
3435
  __metadata("design:type", Function)
2959
- ], OxBoardAIChat.prototype, "boardProvider", void 0);
3436
+ ], OxAssistantChat.prototype, "boardProvider", void 0);
2960
3437
  __decorate([
2961
3438
  property({ attribute: false }),
2962
3439
  __metadata("design:type", Function)
2963
- ], OxBoardAIChat.prototype, "searchProvider", void 0);
3440
+ ], OxAssistantChat.prototype, "searchProvider", void 0);
2964
3441
  __decorate([
2965
3442
  property({ attribute: false }),
2966
3443
  __metadata("design:type", Function)
2967
- ], OxBoardAIChat.prototype, "userProvider", void 0);
3444
+ ], OxAssistantChat.prototype, "userProvider", void 0);
2968
3445
  __decorate([
2969
3446
  property({ type: Array, attribute: false }),
2970
3447
  __metadata("design:type", Array)
2971
- ], OxBoardAIChat.prototype, "selectedRefids", void 0);
3448
+ ], OxAssistantChat.prototype, "selectedRefids", void 0);
2972
3449
  __decorate([
2973
3450
  property({ type: Array, attribute: false }),
2974
3451
  __metadata("design:type", Array)
2975
- ], OxBoardAIChat.prototype, "toolCategories", void 0);
3452
+ ], OxAssistantChat.prototype, "toolCategories", void 0);
2976
3453
  __decorate([
2977
3454
  property({ type: Object, attribute: false }),
2978
3455
  __metadata("design:type", Object)
2979
- ], OxBoardAIChat.prototype, "hostContext", void 0);
3456
+ ], OxAssistantChat.prototype, "hostContext", void 0);
2980
3457
  __decorate([
2981
3458
  property({ type: Boolean, attribute: 'board-tools' }),
2982
3459
  __metadata("design:type", Object)
2983
- ], OxBoardAIChat.prototype, "boardTools", void 0);
3460
+ ], OxAssistantChat.prototype, "boardTools", void 0);
2984
3461
  __decorate([
2985
3462
  property({ type: String, attribute: 'chat-endpoint' }),
2986
3463
  __metadata("design:type", String)
2987
- ], OxBoardAIChat.prototype, "chatEndpoint", void 0);
3464
+ ], OxAssistantChat.prototype, "chatEndpoint", void 0);
2988
3465
  __decorate([
2989
3466
  property({ type: String, attribute: 'system-prompt' }),
2990
3467
  __metadata("design:type", String)
2991
- ], OxBoardAIChat.prototype, "systemPrompt", void 0);
3468
+ ], OxAssistantChat.prototype, "systemPrompt", void 0);
2992
3469
  __decorate([
2993
3470
  property({ type: Boolean, attribute: 'require-grounding-tools' }),
2994
3471
  __metadata("design:type", Object)
2995
- ], OxBoardAIChat.prototype, "requireGroundingTools", void 0);
3472
+ ], OxAssistantChat.prototype, "requireGroundingTools", void 0);
2996
3473
  __decorate([
2997
3474
  property({ type: Array }),
2998
3475
  __metadata("design:type", Array)
2999
- ], OxBoardAIChat.prototype, "scopes", void 0);
3476
+ ], OxAssistantChat.prototype, "scopes", void 0);
3000
3477
  __decorate([
3001
3478
  property({ type: Array, attribute: 'known-types' }),
3002
3479
  __metadata("design:type", Array)
3003
- ], OxBoardAIChat.prototype, "knownTypes", void 0);
3480
+ ], OxAssistantChat.prototype, "knownTypes", void 0);
3004
3481
  __decorate([
3005
3482
  property({ type: Array }),
3006
3483
  __metadata("design:type", Array)
3007
- ], OxBoardAIChat.prototype, "categories", void 0);
3484
+ ], OxAssistantChat.prototype, "categories", void 0);
3008
3485
  __decorate([
3009
3486
  property({ type: Array, attribute: false }),
3010
3487
  __metadata("design:type", Array)
3011
- ], OxBoardAIChat.prototype, "componentSchemas", void 0);
3488
+ ], OxAssistantChat.prototype, "componentSchemas", void 0);
3012
3489
  __decorate([
3013
3490
  property({ type: String }),
3014
3491
  __metadata("design:type", Object)
3015
- ], OxBoardAIChat.prototype, "placeholder", void 0);
3492
+ ], OxAssistantChat.prototype, "placeholder", void 0);
3016
3493
  __decorate([
3017
3494
  state(),
3018
3495
  __metadata("design:type", Array)
3019
- ], OxBoardAIChat.prototype, "lines", void 0);
3496
+ ], OxAssistantChat.prototype, "lines", void 0);
3020
3497
  __decorate([
3021
3498
  state(),
3022
3499
  __metadata("design:type", Object)
3023
- ], OxBoardAIChat.prototype, "input", void 0);
3500
+ ], OxAssistantChat.prototype, "input", void 0);
3024
3501
  __decorate([
3025
3502
  property({ attribute: false }),
3026
3503
  __metadata("design:type", String)
3027
- ], OxBoardAIChat.prototype, "autoAsk", void 0);
3504
+ ], OxAssistantChat.prototype, "autoAsk", void 0);
3028
3505
  __decorate([
3029
3506
  property(),
3030
3507
  __metadata("design:type", String)
3031
- ], OxBoardAIChat.prototype, "autoAskRequestId", void 0);
3508
+ ], OxAssistantChat.prototype, "autoAskRequestId", void 0);
3032
3509
  __decorate([
3033
3510
  property({ attribute: false }),
3034
3511
  __metadata("design:type", Function)
3035
- ], OxBoardAIChat.prototype, "contextProvider", void 0);
3512
+ ], OxAssistantChat.prototype, "contextProvider", void 0);
3036
3513
  __decorate([
3037
3514
  property({ type: Object, attribute: false }),
3038
3515
  __metadata("design:type", Object)
3039
- ], OxBoardAIChat.prototype, "intro", void 0);
3516
+ ], OxAssistantChat.prototype, "intro", void 0);
3040
3517
  __decorate([
3041
3518
  property({ type: Array, attribute: false }),
3042
3519
  __metadata("design:type", Array)
3043
- ], OxBoardAIChat.prototype, "examples", void 0);
3520
+ ], OxAssistantChat.prototype, "examples", void 0);
3044
3521
  __decorate([
3045
3522
  state(),
3046
3523
  __metadata("design:type", String)
3047
- ], OxBoardAIChat.prototype, "lastFailedInput", void 0);
3524
+ ], OxAssistantChat.prototype, "lastFailedInput", void 0);
3525
+ __decorate([
3526
+ state(),
3527
+ __metadata("design:type", Object)
3528
+ ], OxAssistantChat.prototype, "revertedPatchIds", void 0);
3529
+ __decorate([
3530
+ state(),
3531
+ __metadata("design:type", Array)
3532
+ ], OxAssistantChat.prototype, "attachments", void 0);
3048
3533
  __decorate([
3049
3534
  state(),
3050
3535
  __metadata("design:type", Object)
3051
- ], OxBoardAIChat.prototype, "revertedPatchIds", void 0);
3536
+ ], OxAssistantChat.prototype, "dragOver", void 0);
3052
3537
  __decorate([
3053
3538
  state(),
3054
3539
  __metadata("design:type", Object)
3055
- ], OxBoardAIChat.prototype, "examplesOpen", void 0);
3540
+ ], OxAssistantChat.prototype, "examplesOpen", void 0);
3056
3541
  __decorate([
3057
3542
  state(),
3058
3543
  __metadata("design:type", String)
3059
- ], OxBoardAIChat.prototype, "toastMessage", void 0);
3544
+ ], OxAssistantChat.prototype, "toastMessage", void 0);
3060
3545
  __decorate([
3061
3546
  state(),
3062
3547
  __metadata("design:type", Number)
3063
- ], OxBoardAIChat.prototype, "copiedIdx", void 0);
3548
+ ], OxAssistantChat.prototype, "copiedIdx", void 0);
3064
3549
  __decorate([
3065
3550
  state(),
3066
3551
  __metadata("design:type", Object)
3067
- ], OxBoardAIChat.prototype, "openToolSteps", void 0);
3552
+ ], OxAssistantChat.prototype, "openToolSteps", void 0);
3068
3553
  __decorate([
3069
3554
  state(),
3070
3555
  __metadata("design:type", Object)
3071
- ], OxBoardAIChat.prototype, "sentProposals", void 0);
3556
+ ], OxAssistantChat.prototype, "sentProposals", void 0);
3072
3557
  __decorate([
3073
3558
  state(),
3074
3559
  __metadata("design:type", Object)
3075
- ], OxBoardAIChat.prototype, "historyFolded", void 0);
3560
+ ], OxAssistantChat.prototype, "historyFolded", void 0);
3076
3561
  __decorate([
3077
3562
  state(),
3078
3563
  __metadata("design:type", Object)
3079
- ], OxBoardAIChat.prototype, "foldNoticeDismissed", void 0);
3564
+ ], OxAssistantChat.prototype, "foldNoticeDismissed", void 0);
3080
3565
  __decorate([
3081
3566
  state(),
3082
3567
  __metadata("design:type", Object)
3083
- ], OxBoardAIChat.prototype, "busy", void 0);
3568
+ ], OxAssistantChat.prototype, "busy", void 0);
3084
3569
  __decorate([
3085
3570
  state(),
3086
3571
  __metadata("design:type", String)
3087
- ], OxBoardAIChat.prototype, "errorMessage", void 0);
3572
+ ], OxAssistantChat.prototype, "errorMessage", void 0);
3088
3573
  __decorate([
3089
3574
  state(),
3090
3575
  __metadata("design:type", Array)
3091
- ], OxBoardAIChat.prototype, "participants", void 0);
3576
+ ], OxAssistantChat.prototype, "participants", void 0);
3092
3577
  __decorate([
3093
3578
  state(),
3094
3579
  __metadata("design:type", Object)
3095
- ], OxBoardAIChat.prototype, "_presenceTick", void 0);
3580
+ ], OxAssistantChat.prototype, "_presenceTick", void 0);
3096
3581
  __decorate([
3097
3582
  state(),
3098
3583
  __metadata("design:type", Object)
3099
- ], OxBoardAIChat.prototype, "mentionOpen", void 0);
3584
+ ], OxAssistantChat.prototype, "mentionOpen", void 0);
3100
3585
  __decorate([
3101
3586
  state(),
3102
3587
  __metadata("design:type", Array)
3103
- ], OxBoardAIChat.prototype, "mentionResults", void 0);
3588
+ ], OxAssistantChat.prototype, "mentionResults", void 0);
3104
3589
  __decorate([
3105
3590
  state(),
3106
3591
  __metadata("design:type", Object)
3107
- ], OxBoardAIChat.prototype, "mentionActiveIndex", void 0);
3592
+ ], OxAssistantChat.prototype, "mentionActiveIndex", void 0);
3108
3593
  __decorate([
3109
3594
  state(),
3110
3595
  __metadata("design:type", Object)
3111
- ], OxBoardAIChat.prototype, "mentionOverflow", void 0);
3112
- OxBoardAIChat = OxBoardAIChat_1 = __decorate([
3113
- customElement('ox-board-ai-chat')
3114
- ], OxBoardAIChat);
3115
- export { OxBoardAIChat };
3116
- //# sourceMappingURL=board-ai-chat.js.map
3596
+ ], OxAssistantChat.prototype, "mentionOverflow", void 0);
3597
+ OxAssistantChat = OxAssistantChat_1 = __decorate([
3598
+ customElement('ox-assistant-chat')
3599
+ ], OxAssistantChat);
3600
+ export { OxAssistantChat };
3601
+ //# sourceMappingURL=assistant-chat.js.map