@things-factory/ai-assistant 10.1.8 → 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.
- package/client/components/{board-ai-chat.ts → assistant-chat.ts} +597 -66
- package/client/components/assistant-session-toolbar.ts +70 -0
- package/client/components/chat-input-builder.ts +1 -1
- package/client/index.ts +6 -3
- package/client/utils/assistant-request-context.ts +21 -0
- package/client/utils/assistant-session-controller.ts +166 -0
- package/client/utils/assistant-session-transport.ts +34 -0
- package/dist-client/components/{board-ai-chat.d.ts → assistant-chat.d.ts} +30 -3
- package/dist-client/components/{board-ai-chat.js → assistant-chat.js} +616 -109
- package/dist-client/components/assistant-chat.js.map +1 -0
- package/dist-client/components/{board-ai-chat.test.js → assistant-chat.test.js} +1 -1
- package/dist-client/components/assistant-chat.test.js.map +1 -0
- package/dist-client/components/assistant-session-toolbar.d.ts +18 -0
- package/dist-client/components/assistant-session-toolbar.js +109 -0
- package/dist-client/components/assistant-session-toolbar.js.map +1 -0
- package/dist-client/components/chat-input-builder.js +1 -1
- package/dist-client/components/chat-input-builder.js.map +1 -1
- package/dist-client/index.d.ts +6 -3
- package/dist-client/index.js +6 -3
- package/dist-client/index.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/utils/assistant-request-context.d.ts +10 -0
- package/dist-client/utils/assistant-request-context.js +20 -0
- package/dist-client/utils/assistant-request-context.js.map +1 -0
- package/dist-client/utils/assistant-session-controller.d.ts +49 -0
- package/dist-client/utils/assistant-session-controller.js +165 -0
- package/dist-client/utils/assistant-session-controller.js.map +1 -0
- package/dist-client/utils/assistant-session-transport.d.ts +3 -0
- package/dist-client/utils/assistant-session-transport.js +39 -0
- package/dist-client/utils/assistant-session-transport.js.map +1 -0
- package/dist-server/service/assistant-chat-resolver.js +20 -0
- package/dist-server/service/assistant-chat-resolver.js.map +1 -1
- package/dist-server/service/chat-session/chat-session.js +1 -1
- package/dist-server/service/chat-session/chat-session.js.map +1 -1
- package/dist-server/service/chat-session/session-anchor.d.ts +1 -1
- package/dist-server/service/chat-session/session-anchor.js +3 -1
- package/dist-server/service/chat-session/session-anchor.js.map +1 -1
- package/dist-server/service/chat-session/station-session-guard.d.ts +12 -0
- package/dist-server/service/chat-session/station-session-guard.js +20 -0
- package/dist-server/service/chat-session/station-session-guard.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/docs/session-controller.md +52 -0
- package/package.json +2 -2
- package/server/service/assistant-chat-resolver.ts +22 -0
- package/server/service/chat-session/chat-session.ts +1 -1
- package/server/service/chat-session/session-anchor.ts +3 -1
- package/server/service/chat-session/station-session-guard.ts +17 -0
- package/server/service/session-anchor.test.ts +11 -0
- package/server/service/station-session-guard.test.ts +24 -0
- package/test/assistant-request-context.test.ts +30 -0
- package/test/assistant-session-controller.test.ts +88 -0
- package/test/assistant-session-toolbar.test.ts +29 -0
- package/translations/en.json +12 -1
- package/translations/ko.json +12 -1
- package/dist-client/components/board-ai-chat.js.map +0 -1
- package/dist-client/components/board-ai-chat.test.js.map +0 -1
- /package/client/components/{board-ai-chat.test.ts → assistant-chat.test.ts} +0 -0
- /package/dist-client/components/{board-ai-chat.test.d.ts → assistant-chat.test.d.ts} +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
var
|
|
1
|
+
var OxAssistantChat_1;
|
|
2
2
|
import { __decorate, __metadata } from "tslib";
|
|
3
3
|
/**
|
|
4
|
-
* <ox-
|
|
4
|
+
* <ox-assistant-chat> — AI 도우미 채팅 컴포넌트 (Lit).
|
|
5
5
|
*
|
|
6
6
|
* 입력:
|
|
7
7
|
* - sessionId: 영속 ChatSession 식별자 (없으면 ad-hoc 모드, 메시지 영속 안 됨)
|
|
@@ -16,6 +16,8 @@ import { __decorate, __metadata } from "tslib";
|
|
|
16
16
|
* 모드 전환은 컨테이너 (워크스페이스) 의 책임. 이 컴포넌트는 자체로 풀 채팅 UX.
|
|
17
17
|
*/
|
|
18
18
|
import '@material/web/icon/icon.js';
|
|
19
|
+
import './assistant-session-toolbar.js';
|
|
20
|
+
import { assistantAutoAskKey, assistantModelFingerprint } from '../utils/assistant-request-context.js';
|
|
19
21
|
import { LitElement, css, html, nothing } from 'lit';
|
|
20
22
|
import { customElement, property, state } from 'lit/decorators.js';
|
|
21
23
|
import { unsafeHTML } from 'lit/directives/unsafe-html.js';
|
|
@@ -42,6 +44,13 @@ import { buildMentionCandidates, buildCatalogCandidates, buildSlashCandidates, b
|
|
|
42
44
|
*/
|
|
43
45
|
import { chatDefaults } from './chat-defaults.js';
|
|
44
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
|
+
}
|
|
45
54
|
const BOARD_AI_CHAT_MUTATION = gql `
|
|
46
55
|
mutation BoardAIChat($input: BoardAIChatInput!) {
|
|
47
56
|
boardAIChat(input: $input) {
|
|
@@ -152,9 +161,10 @@ function initials(name) {
|
|
|
152
161
|
return (parts[0][0] + parts[1][0]).toUpperCase();
|
|
153
162
|
return /[a-zA-Z]/.test(n) ? n.slice(0, 2).toUpperCase() : n.slice(0, 1);
|
|
154
163
|
}
|
|
155
|
-
let
|
|
164
|
+
let OxAssistantChat = class OxAssistantChat extends LitElement {
|
|
156
165
|
constructor() {
|
|
157
166
|
super(...arguments);
|
|
167
|
+
this.adoptingSession = false;
|
|
158
168
|
/**
|
|
159
169
|
* 보드의 모든 ChatSession 리스트 — 다중 세션 활성화 시 탭 UI 로 표시.
|
|
160
170
|
*
|
|
@@ -203,6 +213,10 @@ let OxBoardAIChat = class OxBoardAIChat extends LitElement {
|
|
|
203
213
|
this.input = '';
|
|
204
214
|
/** 되돌려진 patch id 들 — 한 번 revert 한 patch 는 버튼 비활성 */
|
|
205
215
|
this.revertedPatchIds = new Set();
|
|
216
|
+
/** 첨부된 파일/이미지 목록 */
|
|
217
|
+
this.attachments = [];
|
|
218
|
+
/** 드래그 오버 상태 (시각 피드백용) */
|
|
219
|
+
this.dragOver = false;
|
|
206
220
|
/** mini action — 인라인 예시 토글 */
|
|
207
221
|
this.examplesOpen = false;
|
|
208
222
|
/** 원본 JSON 을 펼친 단계(라인:단계) — 목록은 한 줄 요약으로 훑고 필요한 것만 펼친다. */
|
|
@@ -282,6 +296,8 @@ let OxBoardAIChat = class OxBoardAIChat extends LitElement {
|
|
|
282
296
|
}
|
|
283
297
|
};
|
|
284
298
|
this._onSessionCreateClick = () => {
|
|
299
|
+
if (this.busy || this.lines.length === 0)
|
|
300
|
+
return;
|
|
285
301
|
this.dispatchEvent(new CustomEvent('session-create', {
|
|
286
302
|
bubbles: true,
|
|
287
303
|
composed: true
|
|
@@ -293,10 +309,92 @@ let OxBoardAIChat = class OxBoardAIChat extends LitElement {
|
|
|
293
309
|
*/
|
|
294
310
|
this.startNewFromNotice = () => {
|
|
295
311
|
this.foldNoticeDismissed = true;
|
|
296
|
-
this.dispatchEvent(new CustomEvent('
|
|
312
|
+
this.dispatchEvent(new CustomEvent('assistant-start-new-session', { bubbles: true, composed: true }));
|
|
297
313
|
};
|
|
298
314
|
}
|
|
299
|
-
static {
|
|
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
|
+
}
|
|
300
398
|
/** 현재 로그인 사용자 email — 그룹챗에서 내 메시지 vs 타인 메시지 구분.
|
|
301
399
|
* email 을 쓰는 이유: /auth/profile 이 항상 내려주는 로그인 식별자이고(비어 있지 않다),
|
|
302
400
|
* 내부 관리키(User.id)를 클라이언트 판정에 의존하지 않기 위함. */
|
|
@@ -1087,6 +1185,63 @@ let OxBoardAIChat = class OxBoardAIChat extends LitElement {
|
|
|
1087
1185
|
.proposal.sent {
|
|
1088
1186
|
border-left-color: var(--md-sys-color-outline, #94a3b8);
|
|
1089
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
|
+
}
|
|
1090
1245
|
|
|
1091
1246
|
/* ── 접지 경고 — 근거에 없는 대상을 지목한 답 ───── */
|
|
1092
1247
|
.grounding-warning {
|
|
@@ -1389,10 +1544,156 @@ let OxBoardAIChat = class OxBoardAIChat extends LitElement {
|
|
|
1389
1544
|
|
|
1390
1545
|
/* ── Composer (actions + input 통합 영역) ──────────── */
|
|
1391
1546
|
.composer {
|
|
1547
|
+
position: relative;
|
|
1392
1548
|
border-top: 1px solid var(--md-sys-color-outline-variant, #f1f5f9);
|
|
1393
1549
|
background: var(--md-sys-color-surface, #ffffff);
|
|
1394
1550
|
display: flex;
|
|
1395
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);
|
|
1396
1697
|
}
|
|
1397
1698
|
|
|
1398
1699
|
/* ── Mini action row (메시지 시작 후) ──────────────── */
|
|
@@ -1601,12 +1902,17 @@ let OxBoardAIChat = class OxBoardAIChat extends LitElement {
|
|
|
1601
1902
|
* sessionId 가 붙기 전에 보내면 대화에 남지 않으므로 세션이 준비될 때까지 기다린다.
|
|
1602
1903
|
* 같은 문장을 두 번 보내지 않도록 보낸 값을 기억한다 — 재렌더마다 updated 가 다시 돌기 때문이다.
|
|
1603
1904
|
*/
|
|
1604
|
-
if (this.autoAsk && this.autoAsk !== this._autoAsked && this.sessionId && !this.busy) {
|
|
1605
|
-
this._autoAsked = this.autoAsk;
|
|
1905
|
+
if (this.autoAsk && assistantAutoAskKey(this.autoAsk, this.autoAskRequestId) !== this._autoAsked && (this.sessionId || this.prepareSession) && !this.busy) {
|
|
1906
|
+
this._autoAsked = assistantAutoAskKey(this.autoAsk, this.autoAskRequestId);
|
|
1606
1907
|
this.input = this.autoAsk;
|
|
1607
1908
|
this.send();
|
|
1608
1909
|
}
|
|
1609
|
-
if (changed.has('sessionId')) {
|
|
1910
|
+
if (changed.has('sessionId') && this.adoptingSession) {
|
|
1911
|
+
this.adoptingSession = false;
|
|
1912
|
+
this._startPresence();
|
|
1913
|
+
this._startMessageSubscription();
|
|
1914
|
+
}
|
|
1915
|
+
else if (changed.has('sessionId')) {
|
|
1610
1916
|
// sessionId 가 바뀌면 (set / unset / 다른 보드 전환) 이전 세션의 모든 클라이언트
|
|
1611
1917
|
// 컨텍스트 정리 — lines / pickedMentions / errorMessage / input 모두 새지 않도록.
|
|
1612
1918
|
// 보드 단위 격리 (ChatSession 1:1 boardId) 가 깨지는 회귀 방지.
|
|
@@ -1814,6 +2120,25 @@ let OxBoardAIChat = class OxBoardAIChat extends LitElement {
|
|
|
1814
2120
|
>${content}</span
|
|
1815
2121
|
>`
|
|
1816
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}
|
|
1817
2142
|
${line.patchId
|
|
1818
2143
|
? html `
|
|
1819
2144
|
<div class="summary ${this.revertedPatchIds.has(line.patchId) ? 'reverted' : ''}">
|
|
@@ -1864,11 +2189,47 @@ let OxBoardAIChat = class OxBoardAIChat extends LitElement {
|
|
|
1864
2189
|
`
|
|
1865
2190
|
: nothing}
|
|
1866
2191
|
${this.renderFoldNotice()}
|
|
1867
|
-
<div
|
|
1868
|
-
${
|
|
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
|
|
1869
2230
|
? html `
|
|
1870
|
-
<div class="actions-row">
|
|
1871
2231
|
<button
|
|
2232
|
+
type="button"
|
|
1872
2233
|
class="action"
|
|
1873
2234
|
@click=${() => (this.examplesOpen = !this.examplesOpen)}
|
|
1874
2235
|
title=${i18next.t('ai-assistant.text.show-examples-tooltip')}>
|
|
@@ -1877,8 +2238,32 @@ let OxBoardAIChat = class OxBoardAIChat extends LitElement {
|
|
|
1877
2238
|
? i18next.t('ai-assistant.button.close')
|
|
1878
2239
|
: i18next.t('ai-assistant.button.examples')}
|
|
1879
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
|
+
`)}
|
|
1880
2266
|
</div>
|
|
1881
|
-
${this.examplesOpen ? this.renderInlineExamples() : nothing}
|
|
1882
2267
|
`
|
|
1883
2268
|
: nothing}
|
|
1884
2269
|
<div class="input-row">
|
|
@@ -1915,7 +2300,7 @@ let OxBoardAIChat = class OxBoardAIChat extends LitElement {
|
|
|
1915
2300
|
: nothing}
|
|
1916
2301
|
</div>
|
|
1917
2302
|
<button
|
|
1918
|
-
?disabled=${this.busy || !this.input.trim()}
|
|
2303
|
+
?disabled=${this.busy || (!this.input.trim() && this.attachments.length === 0)}
|
|
1919
2304
|
@click=${this.send}
|
|
1920
2305
|
title=${i18next.t('ai-assistant.text.send-tooltip')}>
|
|
1921
2306
|
${this.busy
|
|
@@ -2190,7 +2575,7 @@ let OxBoardAIChat = class OxBoardAIChat extends LitElement {
|
|
|
2190
2575
|
get exampleGroups() {
|
|
2191
2576
|
if (this.examples?.length)
|
|
2192
2577
|
return this.examples;
|
|
2193
|
-
return
|
|
2578
|
+
return OxAssistantChat_1.EXAMPLE_GROUPS.map(group => ({
|
|
2194
2579
|
label: i18next.t(group.labelKey),
|
|
2195
2580
|
items: group.itemKeys.map(key => i18next.t(key))
|
|
2196
2581
|
}));
|
|
@@ -2232,38 +2617,11 @@ let OxBoardAIChat = class OxBoardAIChat extends LitElement {
|
|
|
2232
2617
|
if (!this.sessions || this.sessions.length === 0)
|
|
2233
2618
|
return nothing;
|
|
2234
2619
|
return html `
|
|
2235
|
-
<
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
defaultValue: `세션 ${i + 1}`
|
|
2241
|
-
});
|
|
2242
|
-
const label = s.name?.trim() || fallback;
|
|
2243
|
-
const tooltip = s.createdAt
|
|
2244
|
-
? `${label} · ${new Date(s.createdAt).toLocaleString()}`
|
|
2245
|
-
: label;
|
|
2246
|
-
return html `
|
|
2247
|
-
<button
|
|
2248
|
-
class="session-tab ${isActive ? 'active' : ''}"
|
|
2249
|
-
role="tab"
|
|
2250
|
-
aria-selected=${isActive ? 'true' : 'false'}
|
|
2251
|
-
title=${tooltip}
|
|
2252
|
-
@click=${() => this._onSessionTabClick(s.id)}>
|
|
2253
|
-
${label}
|
|
2254
|
-
</button>
|
|
2255
|
-
`;
|
|
2256
|
-
})}
|
|
2257
|
-
<button
|
|
2258
|
-
class="session-tab-new"
|
|
2259
|
-
title=${i18next.t('ai-assistant.button.new-chat', {
|
|
2260
|
-
defaultValue: '새 대화'
|
|
2261
|
-
})}
|
|
2262
|
-
@click=${this._onSessionCreateClick}>
|
|
2263
|
-
<md-icon>add</md-icon>
|
|
2264
|
-
<span>${i18next.t('ai-assistant.button.new-chat', { defaultValue: '새 대화' })}</span>
|
|
2265
|
-
</button>
|
|
2266
|
-
</div>
|
|
2620
|
+
<ox-assistant-session-toolbar style="padding:8px 12px" .sessions=${this.sessions}
|
|
2621
|
+
.sessionId=${this.sessionId || ''} .disabled=${this.busy}
|
|
2622
|
+
@assistant-session-select=${(event) => { event.stopPropagation(); this._onSessionTabClick(event.detail.sessionId); }}
|
|
2623
|
+
@assistant-session-reset=${(event) => { event.stopPropagation(); this._onSessionCreateClick(); }}
|
|
2624
|
+
></ox-assistant-session-toolbar>
|
|
2267
2625
|
`;
|
|
2268
2626
|
}
|
|
2269
2627
|
/** C4 roster — 세션 참여자 + online 표시. 참여자 2명 미만이면 숨김(혼자면 의미 없음). */
|
|
@@ -2478,26 +2836,79 @@ let OxBoardAIChat = class OxBoardAIChat extends LitElement {
|
|
|
2478
2836
|
${items.map(p => {
|
|
2479
2837
|
const key = this.proposalKey(p);
|
|
2480
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
|
+
] : []);
|
|
2481
2846
|
return html `
|
|
2482
2847
|
<div class="proposal ${sent ? 'sent' : ''}">
|
|
2483
|
-
<md-icon
|
|
2848
|
+
<md-icon>${p.icon || (p.source ? 'view_in_ar' : 'bolt')}</md-icon>
|
|
2484
2849
|
<span class="pbody">
|
|
2485
2850
|
<span class="plabel">${p.label || p.command}</span>
|
|
2486
2851
|
${p.reason ? html `<span class="preason">${p.reason}</span>` : nothing}
|
|
2487
2852
|
</span>
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
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: '실행 요청됨' })
|
|
2493
2859
|
: i18next.t('ai-assistant.button.run-proposal', { defaultValue: '실행' })}
|
|
2494
|
-
|
|
2860
|
+
</button>
|
|
2861
|
+
` : html `
|
|
2862
|
+
<span class="staged-badge" title="3D 뷰어에 모델이 표시되었습니다">
|
|
2863
|
+
<md-icon>check_circle</md-icon>
|
|
2864
|
+
<span>뷰어 반영됨</span>
|
|
2865
|
+
</span>
|
|
2866
|
+
`}
|
|
2495
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}
|
|
2496
2881
|
`;
|
|
2497
2882
|
})}
|
|
2498
2883
|
</div>
|
|
2499
2884
|
`;
|
|
2500
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
|
+
}
|
|
2501
2912
|
/**
|
|
2502
2913
|
* 제안 식별 키 — 같은 조치를 두 번 실행하지 않도록 **효과**(명령·대상·인자)로만 만든다.
|
|
2503
2914
|
*
|
|
@@ -2529,7 +2940,7 @@ let OxBoardAIChat = class OxBoardAIChat extends LitElement {
|
|
|
2529
2940
|
*/
|
|
2530
2941
|
executeProposal(p, key) {
|
|
2531
2942
|
this.sentProposals = new Set(this.sentProposals).add(key);
|
|
2532
|
-
this.dispatchEvent(new CustomEvent('
|
|
2943
|
+
this.dispatchEvent(new CustomEvent('assistant-proposal-execute', {
|
|
2533
2944
|
detail: { proposal: p, sessionId: this.sessionId },
|
|
2534
2945
|
bubbles: true,
|
|
2535
2946
|
composed: true
|
|
@@ -2794,28 +3205,74 @@ let OxBoardAIChat = class OxBoardAIChat extends LitElement {
|
|
|
2794
3205
|
}
|
|
2795
3206
|
async send() {
|
|
2796
3207
|
const text = this.input.trim();
|
|
2797
|
-
|
|
3208
|
+
const attachmentsToSend = [...this.attachments];
|
|
3209
|
+
if ((!text && attachmentsToSend.length === 0) || this.busy)
|
|
2798
3210
|
return;
|
|
3211
|
+
this.attachments = [];
|
|
3212
|
+
const promptText = text || (attachmentsToSend.length > 0
|
|
3213
|
+
? (attachmentsToSend[0].type.startsWith('image/')
|
|
3214
|
+
? '첨부된 이미지를 참고하여 제안해 주세요.'
|
|
3215
|
+
: '첨부된 파일을 확인해 주세요.')
|
|
3216
|
+
: '');
|
|
3217
|
+
if (!this.sessionId && this.prepareSession) {
|
|
3218
|
+
this.busy = true;
|
|
3219
|
+
try {
|
|
3220
|
+
await this.prepareSession(id => { this.adoptingSession = true; this.sessionId = id; });
|
|
3221
|
+
await this.updateComplete;
|
|
3222
|
+
}
|
|
3223
|
+
catch (error) {
|
|
3224
|
+
this.errorMessage = error instanceof Error ? error.message : String(error);
|
|
3225
|
+
this.busy = false;
|
|
3226
|
+
return;
|
|
3227
|
+
}
|
|
3228
|
+
this.busy = false;
|
|
3229
|
+
}
|
|
3230
|
+
let requestContext;
|
|
3231
|
+
let liveBoard;
|
|
3232
|
+
let requestBase;
|
|
3233
|
+
try {
|
|
3234
|
+
const provided = this.contextProvider?.() || {};
|
|
3235
|
+
requestContext = structuredClone({ systemPrompt: this.systemPrompt, hostContext: this.hostContext, ...provided });
|
|
3236
|
+
liveBoard = structuredClone(this.boardProvider ? this.boardProvider() : this.currentBoard);
|
|
3237
|
+
requestBase = { version: 1, sessionId: this.sessionId, hostContext: requestContext.hostContext,
|
|
3238
|
+
modelFingerprint: assistantModelFingerprint(liveBoard) };
|
|
3239
|
+
}
|
|
3240
|
+
catch (error) {
|
|
3241
|
+
this.errorMessage = error instanceof Error ? error.message : String(error);
|
|
3242
|
+
return;
|
|
3243
|
+
}
|
|
2799
3244
|
// pickedMentions / pickedUserMentions 에서 현재 텍스트에 실제 #/@ token 으로
|
|
2800
3245
|
// 등장하는 것만 추출 — 지운 토큰 제외. inline 마커로 content 에 주입.
|
|
2801
3246
|
const mentions = [];
|
|
2802
3247
|
for (const [token, refid] of this.pickedMentions.entries()) {
|
|
2803
|
-
if (
|
|
3248
|
+
if (promptText.includes(`#${token}`))
|
|
2804
3249
|
mentions.push({ token, refid });
|
|
2805
3250
|
}
|
|
2806
3251
|
const userMentions = [];
|
|
2807
3252
|
for (const [token, userId] of this.pickedUserMentions.entries()) {
|
|
2808
|
-
if (
|
|
3253
|
+
if (promptText.includes(`@${token}`))
|
|
2809
3254
|
userMentions.push({ token, userId });
|
|
2810
3255
|
}
|
|
2811
|
-
let enrichedContent = injectMentionRefids(
|
|
3256
|
+
let enrichedContent = injectMentionRefids(promptText, mentions);
|
|
2812
3257
|
enrichedContent = injectMentionUserIds(enrichedContent, userMentions);
|
|
2813
3258
|
// optimistic 라인에 임시 핸들 — 방송 에코와의 reconcile 을 index 비의존으로.
|
|
2814
3259
|
const userLocalId = `local-${++this._localSeq}-u`;
|
|
2815
3260
|
const asstLocalId = `local-${++this._localSeq}-a`;
|
|
2816
3261
|
this.lines = [
|
|
2817
3262
|
...this.lines,
|
|
2818
|
-
{
|
|
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
|
+
}
|
|
2819
3276
|
];
|
|
2820
3277
|
this.input = '';
|
|
2821
3278
|
this.busy = true;
|
|
@@ -2823,6 +3280,24 @@ let OxBoardAIChat = class OxBoardAIChat extends LitElement {
|
|
|
2823
3280
|
// 가짜 pending 응답 표시 (UX 부드럽게)
|
|
2824
3281
|
this.lines = [...this.lines, { role: 'assistant', content: '', pending: true, _localId: asstLocalId }];
|
|
2825
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
|
+
}
|
|
2826
3301
|
// LLM 으로 보낼 history — user/assistant 만 (system 은 백엔드가 자동 합류).
|
|
2827
3302
|
// pending placeholder 와 system 은 제외 (index 비의존 — 방송 append 와 무관).
|
|
2828
3303
|
const history = this.lines
|
|
@@ -2830,7 +3305,6 @@ let OxBoardAIChat = class OxBoardAIChat extends LitElement {
|
|
|
2830
3305
|
.map(l => ({ role: l.role, content: l.content }));
|
|
2831
3306
|
// 라이브 보드 우선 — 호스트의 캔버스가 사용자 수작업 편집을 들고 있을 수 있음.
|
|
2832
3307
|
// boardProvider 가 있으면 send 시점에 그것을 pull, 없으면 정적 currentBoard.
|
|
2833
|
-
const liveBoard = this.boardProvider ? this.boardProvider() : this.currentBoard;
|
|
2834
3308
|
// mentions 는 위에서 이미 계산해 user line 에 thread — 그대로 mutation 입력으로 재사용.
|
|
2835
3309
|
const neutral = this.chatEndpoint === 'assistantChat';
|
|
2836
3310
|
const result = await client.mutate({
|
|
@@ -2840,9 +3314,9 @@ let OxBoardAIChat = class OxBoardAIChat extends LitElement {
|
|
|
2840
3314
|
? buildAssistantChatInput({
|
|
2841
3315
|
sessionId: this.sessionId,
|
|
2842
3316
|
history,
|
|
2843
|
-
systemPrompt:
|
|
3317
|
+
systemPrompt: requestContext.systemPrompt,
|
|
2844
3318
|
truncateAfterMessageId: this._truncateAfterId,
|
|
2845
|
-
hostContext:
|
|
3319
|
+
hostContext: requestContext.hostContext,
|
|
2846
3320
|
toolCategories: this.toolCategories,
|
|
2847
3321
|
requireGroundingTools: this.requireGroundingTools
|
|
2848
3322
|
})
|
|
@@ -2852,7 +3326,7 @@ let OxBoardAIChat = class OxBoardAIChat extends LitElement {
|
|
|
2852
3326
|
liveBoard,
|
|
2853
3327
|
scopes: this.scopes,
|
|
2854
3328
|
truncateAfterMessageId: this._truncateAfterId,
|
|
2855
|
-
hostContext:
|
|
3329
|
+
hostContext: requestContext.hostContext,
|
|
2856
3330
|
toolCategories: this.toolCategories,
|
|
2857
3331
|
boardTools: this.boardTools,
|
|
2858
3332
|
requireGroundingTools: this.requireGroundingTools,
|
|
@@ -2886,11 +3360,24 @@ let OxBoardAIChat = class OxBoardAIChat extends LitElement {
|
|
|
2886
3360
|
proposals: Array.isArray(out.proposals) ? out.proposals : undefined,
|
|
2887
3361
|
pending: false
|
|
2888
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
|
+
}
|
|
2889
3375
|
// 호스트로 patch 이벤트 전파
|
|
2890
3376
|
if (out.patch) {
|
|
2891
3377
|
this.dispatchEvent(new CustomEvent('board-edit-patch', {
|
|
2892
3378
|
detail: {
|
|
2893
3379
|
patch: out.patch,
|
|
3380
|
+
requestBase,
|
|
2894
3381
|
summary: out.patch.summary,
|
|
2895
3382
|
confidence: out.patch.confidence,
|
|
2896
3383
|
patchId: out.patchId,
|
|
@@ -2903,7 +3390,7 @@ let OxBoardAIChat = class OxBoardAIChat extends LitElement {
|
|
|
2903
3390
|
// 호스트로 ephemeral scene 조작 actions 전파 (selection / view / mode)
|
|
2904
3391
|
if (Array.isArray(out.actions) && out.actions.length > 0) {
|
|
2905
3392
|
this.dispatchEvent(new CustomEvent('board-action-execute', {
|
|
2906
|
-
detail: { actions: out.actions, sessionId: out.sessionId },
|
|
3393
|
+
detail: { actions: out.actions, sessionId: out.sessionId, requestBase },
|
|
2907
3394
|
bubbles: true,
|
|
2908
3395
|
composed: true
|
|
2909
3396
|
}));
|
|
@@ -2930,165 +3417,185 @@ let OxBoardAIChat = class OxBoardAIChat extends LitElement {
|
|
|
2930
3417
|
__decorate([
|
|
2931
3418
|
property({ type: String, attribute: 'session-id' }),
|
|
2932
3419
|
__metadata("design:type", String)
|
|
2933
|
-
],
|
|
3420
|
+
], OxAssistantChat.prototype, "sessionId", void 0);
|
|
3421
|
+
__decorate([
|
|
3422
|
+
property({ attribute: false }),
|
|
3423
|
+
__metadata("design:type", Function)
|
|
3424
|
+
], OxAssistantChat.prototype, "prepareSession", void 0);
|
|
2934
3425
|
__decorate([
|
|
2935
3426
|
property({ type: Array, attribute: false }),
|
|
2936
3427
|
__metadata("design:type", Array)
|
|
2937
|
-
],
|
|
3428
|
+
], OxAssistantChat.prototype, "sessions", void 0);
|
|
2938
3429
|
__decorate([
|
|
2939
3430
|
property({ type: Object }),
|
|
2940
3431
|
__metadata("design:type", Object)
|
|
2941
|
-
],
|
|
3432
|
+
], OxAssistantChat.prototype, "currentBoard", void 0);
|
|
2942
3433
|
__decorate([
|
|
2943
3434
|
property({ attribute: false }),
|
|
2944
3435
|
__metadata("design:type", Function)
|
|
2945
|
-
],
|
|
3436
|
+
], OxAssistantChat.prototype, "boardProvider", void 0);
|
|
2946
3437
|
__decorate([
|
|
2947
3438
|
property({ attribute: false }),
|
|
2948
3439
|
__metadata("design:type", Function)
|
|
2949
|
-
],
|
|
3440
|
+
], OxAssistantChat.prototype, "searchProvider", void 0);
|
|
2950
3441
|
__decorate([
|
|
2951
3442
|
property({ attribute: false }),
|
|
2952
3443
|
__metadata("design:type", Function)
|
|
2953
|
-
],
|
|
3444
|
+
], OxAssistantChat.prototype, "userProvider", void 0);
|
|
2954
3445
|
__decorate([
|
|
2955
3446
|
property({ type: Array, attribute: false }),
|
|
2956
3447
|
__metadata("design:type", Array)
|
|
2957
|
-
],
|
|
3448
|
+
], OxAssistantChat.prototype, "selectedRefids", void 0);
|
|
2958
3449
|
__decorate([
|
|
2959
3450
|
property({ type: Array, attribute: false }),
|
|
2960
3451
|
__metadata("design:type", Array)
|
|
2961
|
-
],
|
|
3452
|
+
], OxAssistantChat.prototype, "toolCategories", void 0);
|
|
2962
3453
|
__decorate([
|
|
2963
3454
|
property({ type: Object, attribute: false }),
|
|
2964
3455
|
__metadata("design:type", Object)
|
|
2965
|
-
],
|
|
3456
|
+
], OxAssistantChat.prototype, "hostContext", void 0);
|
|
2966
3457
|
__decorate([
|
|
2967
3458
|
property({ type: Boolean, attribute: 'board-tools' }),
|
|
2968
3459
|
__metadata("design:type", Object)
|
|
2969
|
-
],
|
|
3460
|
+
], OxAssistantChat.prototype, "boardTools", void 0);
|
|
2970
3461
|
__decorate([
|
|
2971
3462
|
property({ type: String, attribute: 'chat-endpoint' }),
|
|
2972
3463
|
__metadata("design:type", String)
|
|
2973
|
-
],
|
|
3464
|
+
], OxAssistantChat.prototype, "chatEndpoint", void 0);
|
|
2974
3465
|
__decorate([
|
|
2975
3466
|
property({ type: String, attribute: 'system-prompt' }),
|
|
2976
3467
|
__metadata("design:type", String)
|
|
2977
|
-
],
|
|
3468
|
+
], OxAssistantChat.prototype, "systemPrompt", void 0);
|
|
2978
3469
|
__decorate([
|
|
2979
3470
|
property({ type: Boolean, attribute: 'require-grounding-tools' }),
|
|
2980
3471
|
__metadata("design:type", Object)
|
|
2981
|
-
],
|
|
3472
|
+
], OxAssistantChat.prototype, "requireGroundingTools", void 0);
|
|
2982
3473
|
__decorate([
|
|
2983
3474
|
property({ type: Array }),
|
|
2984
3475
|
__metadata("design:type", Array)
|
|
2985
|
-
],
|
|
3476
|
+
], OxAssistantChat.prototype, "scopes", void 0);
|
|
2986
3477
|
__decorate([
|
|
2987
3478
|
property({ type: Array, attribute: 'known-types' }),
|
|
2988
3479
|
__metadata("design:type", Array)
|
|
2989
|
-
],
|
|
3480
|
+
], OxAssistantChat.prototype, "knownTypes", void 0);
|
|
2990
3481
|
__decorate([
|
|
2991
3482
|
property({ type: Array }),
|
|
2992
3483
|
__metadata("design:type", Array)
|
|
2993
|
-
],
|
|
3484
|
+
], OxAssistantChat.prototype, "categories", void 0);
|
|
2994
3485
|
__decorate([
|
|
2995
3486
|
property({ type: Array, attribute: false }),
|
|
2996
3487
|
__metadata("design:type", Array)
|
|
2997
|
-
],
|
|
3488
|
+
], OxAssistantChat.prototype, "componentSchemas", void 0);
|
|
2998
3489
|
__decorate([
|
|
2999
3490
|
property({ type: String }),
|
|
3000
3491
|
__metadata("design:type", Object)
|
|
3001
|
-
],
|
|
3492
|
+
], OxAssistantChat.prototype, "placeholder", void 0);
|
|
3002
3493
|
__decorate([
|
|
3003
3494
|
state(),
|
|
3004
3495
|
__metadata("design:type", Array)
|
|
3005
|
-
],
|
|
3496
|
+
], OxAssistantChat.prototype, "lines", void 0);
|
|
3006
3497
|
__decorate([
|
|
3007
3498
|
state(),
|
|
3008
3499
|
__metadata("design:type", Object)
|
|
3009
|
-
],
|
|
3500
|
+
], OxAssistantChat.prototype, "input", void 0);
|
|
3010
3501
|
__decorate([
|
|
3011
3502
|
property({ attribute: false }),
|
|
3012
3503
|
__metadata("design:type", String)
|
|
3013
|
-
],
|
|
3504
|
+
], OxAssistantChat.prototype, "autoAsk", void 0);
|
|
3505
|
+
__decorate([
|
|
3506
|
+
property(),
|
|
3507
|
+
__metadata("design:type", String)
|
|
3508
|
+
], OxAssistantChat.prototype, "autoAskRequestId", void 0);
|
|
3509
|
+
__decorate([
|
|
3510
|
+
property({ attribute: false }),
|
|
3511
|
+
__metadata("design:type", Function)
|
|
3512
|
+
], OxAssistantChat.prototype, "contextProvider", void 0);
|
|
3014
3513
|
__decorate([
|
|
3015
3514
|
property({ type: Object, attribute: false }),
|
|
3016
3515
|
__metadata("design:type", Object)
|
|
3017
|
-
],
|
|
3516
|
+
], OxAssistantChat.prototype, "intro", void 0);
|
|
3018
3517
|
__decorate([
|
|
3019
3518
|
property({ type: Array, attribute: false }),
|
|
3020
3519
|
__metadata("design:type", Array)
|
|
3021
|
-
],
|
|
3520
|
+
], OxAssistantChat.prototype, "examples", void 0);
|
|
3022
3521
|
__decorate([
|
|
3023
3522
|
state(),
|
|
3024
3523
|
__metadata("design:type", String)
|
|
3025
|
-
],
|
|
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);
|
|
3026
3533
|
__decorate([
|
|
3027
3534
|
state(),
|
|
3028
3535
|
__metadata("design:type", Object)
|
|
3029
|
-
],
|
|
3536
|
+
], OxAssistantChat.prototype, "dragOver", void 0);
|
|
3030
3537
|
__decorate([
|
|
3031
3538
|
state(),
|
|
3032
3539
|
__metadata("design:type", Object)
|
|
3033
|
-
],
|
|
3540
|
+
], OxAssistantChat.prototype, "examplesOpen", void 0);
|
|
3034
3541
|
__decorate([
|
|
3035
3542
|
state(),
|
|
3036
3543
|
__metadata("design:type", String)
|
|
3037
|
-
],
|
|
3544
|
+
], OxAssistantChat.prototype, "toastMessage", void 0);
|
|
3038
3545
|
__decorate([
|
|
3039
3546
|
state(),
|
|
3040
3547
|
__metadata("design:type", Number)
|
|
3041
|
-
],
|
|
3548
|
+
], OxAssistantChat.prototype, "copiedIdx", void 0);
|
|
3042
3549
|
__decorate([
|
|
3043
3550
|
state(),
|
|
3044
3551
|
__metadata("design:type", Object)
|
|
3045
|
-
],
|
|
3552
|
+
], OxAssistantChat.prototype, "openToolSteps", void 0);
|
|
3046
3553
|
__decorate([
|
|
3047
3554
|
state(),
|
|
3048
3555
|
__metadata("design:type", Object)
|
|
3049
|
-
],
|
|
3556
|
+
], OxAssistantChat.prototype, "sentProposals", void 0);
|
|
3050
3557
|
__decorate([
|
|
3051
3558
|
state(),
|
|
3052
3559
|
__metadata("design:type", Object)
|
|
3053
|
-
],
|
|
3560
|
+
], OxAssistantChat.prototype, "historyFolded", void 0);
|
|
3054
3561
|
__decorate([
|
|
3055
3562
|
state(),
|
|
3056
3563
|
__metadata("design:type", Object)
|
|
3057
|
-
],
|
|
3564
|
+
], OxAssistantChat.prototype, "foldNoticeDismissed", void 0);
|
|
3058
3565
|
__decorate([
|
|
3059
3566
|
state(),
|
|
3060
3567
|
__metadata("design:type", Object)
|
|
3061
|
-
],
|
|
3568
|
+
], OxAssistantChat.prototype, "busy", void 0);
|
|
3062
3569
|
__decorate([
|
|
3063
3570
|
state(),
|
|
3064
3571
|
__metadata("design:type", String)
|
|
3065
|
-
],
|
|
3572
|
+
], OxAssistantChat.prototype, "errorMessage", void 0);
|
|
3066
3573
|
__decorate([
|
|
3067
3574
|
state(),
|
|
3068
3575
|
__metadata("design:type", Array)
|
|
3069
|
-
],
|
|
3576
|
+
], OxAssistantChat.prototype, "participants", void 0);
|
|
3070
3577
|
__decorate([
|
|
3071
3578
|
state(),
|
|
3072
3579
|
__metadata("design:type", Object)
|
|
3073
|
-
],
|
|
3580
|
+
], OxAssistantChat.prototype, "_presenceTick", void 0);
|
|
3074
3581
|
__decorate([
|
|
3075
3582
|
state(),
|
|
3076
3583
|
__metadata("design:type", Object)
|
|
3077
|
-
],
|
|
3584
|
+
], OxAssistantChat.prototype, "mentionOpen", void 0);
|
|
3078
3585
|
__decorate([
|
|
3079
3586
|
state(),
|
|
3080
3587
|
__metadata("design:type", Array)
|
|
3081
|
-
],
|
|
3588
|
+
], OxAssistantChat.prototype, "mentionResults", void 0);
|
|
3082
3589
|
__decorate([
|
|
3083
3590
|
state(),
|
|
3084
3591
|
__metadata("design:type", Object)
|
|
3085
|
-
],
|
|
3592
|
+
], OxAssistantChat.prototype, "mentionActiveIndex", void 0);
|
|
3086
3593
|
__decorate([
|
|
3087
3594
|
state(),
|
|
3088
3595
|
__metadata("design:type", Object)
|
|
3089
|
-
],
|
|
3090
|
-
|
|
3091
|
-
customElement('ox-
|
|
3092
|
-
],
|
|
3093
|
-
export {
|
|
3094
|
-
//# sourceMappingURL=
|
|
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
|