@things-factory/ai-assistant 10.1.6 → 10.1.7
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 +165 -100
- package/client/components/chat-defaults.ts +13 -0
- package/client/components/chat-input-builder.test.ts +81 -0
- package/client/components/chat-input-builder.ts +43 -0
- package/client/components/chat-triggers.test.ts +72 -0
- package/client/components/chat-triggers.ts +62 -0
- package/dist-client/components/board-ai-chat.d.ts +8 -0
- package/dist-client/components/board-ai-chat.js +160 -100
- package/dist-client/components/board-ai-chat.js.map +1 -1
- package/dist-client/components/chat-defaults.d.ts +12 -0
- package/dist-client/components/chat-defaults.js +2 -0
- package/dist-client/components/chat-defaults.js.map +1 -1
- package/dist-client/components/chat-input-builder.d.ts +29 -0
- package/dist-client/components/chat-input-builder.js +22 -0
- package/dist-client/components/chat-input-builder.js.map +1 -1
- package/dist-client/components/chat-input-builder.test.d.ts +1 -0
- package/dist-client/components/chat-input-builder.test.js +67 -0
- package/dist-client/components/chat-input-builder.test.js.map +1 -0
- package/dist-client/components/chat-triggers.d.ts +16 -0
- package/dist-client/components/chat-triggers.js +42 -0
- package/dist-client/components/chat-triggers.js.map +1 -0
- package/dist-client/components/chat-triggers.test.d.ts +1 -0
- package/dist-client/components/chat-triggers.test.js +61 -0
- package/dist-client/components/chat-triggers.test.js.map +1 -0
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/service/assistant-chat-resolver.d.ts +32 -0
- package/dist-server/service/assistant-chat-resolver.js +342 -0
- package/dist-server/service/assistant-chat-resolver.js.map +1 -0
- package/dist-server/service/chat-session-resolver.js +18 -18
- package/dist-server/service/chat-session-resolver.js.map +1 -1
- package/dist-server/service/index.d.ts +3 -1
- package/dist-server/service/index.js +4 -0
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/server/service/assistant-chat-resolver.ts +326 -0
- package/server/service/chat-session-resolver.ts +18 -18
- package/server/service/index.ts +4 -0
- package/test/translations.test.ts +79 -0
- package/translations/en.json +59 -0
- package/translations/ja.json +58 -0
- package/translations/ko.json +59 -0
- package/translations/ms.json +58 -0
- package/translations/zh.json +58 -0
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
injectMentionUserIds,
|
|
31
31
|
stripMentionRefids
|
|
32
32
|
} from './markdown.js'
|
|
33
|
-
import { buildChatMutationInput } from './chat-input-builder.js'
|
|
33
|
+
import { buildAssistantChatInput, buildChatMutationInput } from './chat-input-builder.js'
|
|
34
34
|
import { receiveRemoteMessage, reconcileSentLine, remoteMessageToLine } from './chat-echo-dedup.js'
|
|
35
35
|
import './mention-popup.js'
|
|
36
36
|
import {
|
|
@@ -57,6 +57,7 @@ import {
|
|
|
57
57
|
* 뜨지 않는 것이 맞다.
|
|
58
58
|
*/
|
|
59
59
|
import { chatDefaults } from './chat-defaults.js'
|
|
60
|
+
import { availableTriggers, type TriggerChip } from './chat-triggers.js'
|
|
60
61
|
|
|
61
62
|
interface ChatLine {
|
|
62
63
|
role: 'user' | 'assistant' | 'system'
|
|
@@ -125,6 +126,30 @@ const BOARD_AI_CHAT_MUTATION = gql`
|
|
|
125
126
|
}
|
|
126
127
|
`
|
|
127
128
|
|
|
129
|
+
/*
|
|
130
|
+
* The neutral door. Selected by `chatEndpoint`, and it asks for fewer fields because there is
|
|
131
|
+
* less to ask for: no patch, no scene actions, no patch id — this surface has no document.
|
|
132
|
+
*
|
|
133
|
+
* `offeredTools` has no counterpart on the board side. It is what the server actually put in
|
|
134
|
+
* front of the model this turn, so a surface that granted a category and sees it missing knows
|
|
135
|
+
* the registration never happened, instead of reading a vague answer as a refusal.
|
|
136
|
+
*/
|
|
137
|
+
const ASSISTANT_CHAT_MUTATION = gql`
|
|
138
|
+
mutation AssistantChat($input: AssistantChatInput!) {
|
|
139
|
+
assistantChat(input: $input) {
|
|
140
|
+
reply
|
|
141
|
+
clientId
|
|
142
|
+
sessionId
|
|
143
|
+
userMessageId
|
|
144
|
+
assistantMessageId
|
|
145
|
+
toolUsages
|
|
146
|
+
offeredTools
|
|
147
|
+
proposals
|
|
148
|
+
groundingWarnings
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
`
|
|
152
|
+
|
|
128
153
|
const CHAT_MESSAGES_QUERY = gql`
|
|
129
154
|
query ChatMessages($sessionId: String!, $limit: Int, $offset: Int) {
|
|
130
155
|
chatMessages(sessionId: $sessionId, limit: $limit, offset: $offset) {
|
|
@@ -291,6 +316,31 @@ export class OxBoardAIChat extends LitElement {
|
|
|
291
316
|
@property({ type: Boolean, attribute: 'board-tools' })
|
|
292
317
|
boardTools = true
|
|
293
318
|
|
|
319
|
+
/*
|
|
320
|
+
* Which server operation this surface talks to. A closed set of two, named after the operations
|
|
321
|
+
* themselves so a reader can grep either way.
|
|
322
|
+
*
|
|
323
|
+
* boardAIChat board authoring — lives in @things-factory/board-ai, carries the board model,
|
|
324
|
+
* returns patches and scene actions
|
|
325
|
+
* assistantChat a surface with no document of its own — lives here, runs registered tools only
|
|
326
|
+
*
|
|
327
|
+
* Default stays `boardAIChat`: every existing consumer is a board surface, and a default that
|
|
328
|
+
* changed under them would move their conversation to a door with no board tools behind it.
|
|
329
|
+
*
|
|
330
|
+
* A host that has no board must pass `assistantChat` **and** `systemPrompt`. Until it does, the
|
|
331
|
+
* chat calls a mutation that lives in a package it may not even load — which is how
|
|
332
|
+
* operato-figure got `Cannot query field "boardAIChat" on type "Mutation"` (2026-09-11).
|
|
333
|
+
*/
|
|
334
|
+
@property({ type: String, attribute: 'chat-endpoint' })
|
|
335
|
+
chatEndpoint: 'boardAIChat' | 'assistantChat' = 'boardAIChat'
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Who the assistant is on this surface, in the host's words. Required by `assistantChat`; the
|
|
339
|
+
* board conversation carries its own persona on the server and ignores this.
|
|
340
|
+
*/
|
|
341
|
+
@property({ type: String, attribute: 'system-prompt' })
|
|
342
|
+
systemPrompt?: string
|
|
343
|
+
|
|
294
344
|
/**
|
|
295
345
|
* 라이브 상태를 다루는 대화면인가 — true 면 **첫 턴에 도구 호출을 강제**한다.
|
|
296
346
|
*
|
|
@@ -1953,7 +2003,7 @@ export class OxBoardAIChat extends LitElement {
|
|
|
1953
2003
|
// 로드된 메시지 id 를 seen 에 등록 — 이후 방송 에코 dedup 기준.
|
|
1954
2004
|
this._seenMessageIds = new Set<string>(msgs.map((m: any) => m.id).filter(Boolean))
|
|
1955
2005
|
} catch (e: any) {
|
|
1956
|
-
this.errorMessage = `${i18next.t('
|
|
2006
|
+
this.errorMessage = `${i18next.t('ai-assistant.text.failed-to-load-history')}: ${e.message ?? e}`
|
|
1957
2007
|
}
|
|
1958
2008
|
}
|
|
1959
2009
|
|
|
@@ -1984,9 +2034,9 @@ export class OxBoardAIChat extends LitElement {
|
|
|
1984
2034
|
const showActions = line.role !== 'system' && !line.pending && content.length > 0
|
|
1985
2035
|
return html`
|
|
1986
2036
|
${showLabel
|
|
1987
|
-
? html`<div class="msg-label">${i18next.t('
|
|
2037
|
+
? html`<div class="msg-label">${i18next.t('ai-assistant.label.ai')}</div>`
|
|
1988
2038
|
: showSenderLabel
|
|
1989
|
-
? html`<div class="msg-label sender">${line.senderName || i18next.t('
|
|
2039
|
+
? html`<div class="msg-label sender">${line.senderName || i18next.t('ai-assistant.label.participant')}</div>`
|
|
1990
2040
|
: nothing}
|
|
1991
2041
|
<div class="msg-wrap ${line.role} ${isOther ? 'other' : ''}">
|
|
1992
2042
|
<div
|
|
@@ -2000,7 +2050,7 @@ export class OxBoardAIChat extends LitElement {
|
|
|
2000
2050
|
: line.role === 'system'
|
|
2001
2051
|
? html`<span
|
|
2002
2052
|
class="sys-text"
|
|
2003
|
-
title=${i18next.t('
|
|
2053
|
+
title=${i18next.t('ai-assistant.text.toggle-system-note', { defaultValue: '눌러서 펼치기' })}
|
|
2004
2054
|
@click=${() => this.toggleSystemNote(idx)}
|
|
2005
2055
|
>${content}</span
|
|
2006
2056
|
>`
|
|
@@ -2011,14 +2061,14 @@ export class OxBoardAIChat extends LitElement {
|
|
|
2011
2061
|
<md-icon>check_circle</md-icon>
|
|
2012
2062
|
<span class="summary-text">
|
|
2013
2063
|
${this.revertedPatchIds.has(line.patchId)
|
|
2014
|
-
? i18next.t('
|
|
2015
|
-
: i18next.t('
|
|
2064
|
+
? i18next.t('ai-assistant.text.reverted')
|
|
2065
|
+
: i18next.t('ai-assistant.text.applied-changes')}
|
|
2016
2066
|
</span>
|
|
2017
2067
|
<button
|
|
2018
2068
|
class="revert-btn"
|
|
2019
2069
|
?disabled=${this.revertedPatchIds.has(line.patchId)}
|
|
2020
2070
|
@click=${() => line.patchId && this.onRevertClick(line.patchId)}>
|
|
2021
|
-
${i18next.t('
|
|
2071
|
+
${i18next.t('ai-assistant.button.revert')}
|
|
2022
2072
|
</button>
|
|
2023
2073
|
</div>
|
|
2024
2074
|
`
|
|
@@ -2047,7 +2097,7 @@ export class OxBoardAIChat extends LitElement {
|
|
|
2047
2097
|
${this.lastFailedInput
|
|
2048
2098
|
? html`
|
|
2049
2099
|
<button class="retry-btn" @click=${this.retryLastSend}>
|
|
2050
|
-
${i18next.t('
|
|
2100
|
+
${i18next.t('ai-assistant.button.retry')}
|
|
2051
2101
|
</button>
|
|
2052
2102
|
`
|
|
2053
2103
|
: nothing}
|
|
@@ -2062,11 +2112,11 @@ export class OxBoardAIChat extends LitElement {
|
|
|
2062
2112
|
<button
|
|
2063
2113
|
class="action"
|
|
2064
2114
|
@click=${() => (this.examplesOpen = !this.examplesOpen)}
|
|
2065
|
-
title=${i18next.t('
|
|
2115
|
+
title=${i18next.t('ai-assistant.text.show-examples-tooltip')}>
|
|
2066
2116
|
<md-icon>${this.examplesOpen ? 'close' : 'lightbulb'}</md-icon>
|
|
2067
2117
|
${this.examplesOpen
|
|
2068
|
-
? i18next.t('
|
|
2069
|
-
: i18next.t('
|
|
2118
|
+
? i18next.t('ai-assistant.button.close')
|
|
2119
|
+
: i18next.t('ai-assistant.button.examples')}
|
|
2070
2120
|
</button>
|
|
2071
2121
|
</div>
|
|
2072
2122
|
${this.examplesOpen ? this.renderInlineExamples() : nothing}
|
|
@@ -2085,7 +2135,7 @@ export class OxBoardAIChat extends LitElement {
|
|
|
2085
2135
|
}}
|
|
2086
2136
|
?disabled=${this.busy}
|
|
2087
2137
|
rows="1"
|
|
2088
|
-
placeholder=${this.placeholder ?? i18next.t('
|
|
2138
|
+
placeholder=${this.placeholder ?? i18next.t('ai-assistant.text.input-placeholder')}></textarea>
|
|
2089
2139
|
${this.mentionOpen
|
|
2090
2140
|
? html`
|
|
2091
2141
|
<ox-mention-popup
|
|
@@ -2109,7 +2159,7 @@ export class OxBoardAIChat extends LitElement {
|
|
|
2109
2159
|
<button
|
|
2110
2160
|
?disabled=${this.busy || !this.input.trim()}
|
|
2111
2161
|
@click=${this.send}
|
|
2112
|
-
title=${i18next.t('
|
|
2162
|
+
title=${i18next.t('ai-assistant.text.send-tooltip')}>
|
|
2113
2163
|
${this.busy
|
|
2114
2164
|
? html`<span class="spinner"></span>`
|
|
2115
2165
|
: html`<md-icon>arrow_upward</md-icon>`}
|
|
@@ -2343,7 +2393,7 @@ export class OxBoardAIChat extends LitElement {
|
|
|
2343
2393
|
])
|
|
2344
2394
|
} else {
|
|
2345
2395
|
this.showToast(
|
|
2346
|
-
i18next.t('
|
|
2396
|
+
i18next.t('ai-assistant.text.mention-not-found', {
|
|
2347
2397
|
token,
|
|
2348
2398
|
defaultValue: `#${token} 을(를) 찾을 수 없습니다`
|
|
2349
2399
|
})
|
|
@@ -2438,29 +2488,38 @@ export class OxBoardAIChat extends LitElement {
|
|
|
2438
2488
|
}))
|
|
2439
2489
|
}
|
|
2440
2490
|
|
|
2491
|
+
/** 이 표면이 실제로 쓸 수 있는 트리거 — 판정은 `chat-triggers.ts` 가 하고 여기서는 사실만 읽는다. */
|
|
2492
|
+
private get triggerChips(): TriggerChip[] {
|
|
2493
|
+
return availableTriggers({
|
|
2494
|
+
hasMentionableThings: !!(this.boardProvider || this.currentBoard || this.knownTypes?.length),
|
|
2495
|
+
hasSession: !!this.sessionId,
|
|
2496
|
+
slashTemplateCount: chatDefaults.slashTemplates.length
|
|
2497
|
+
})
|
|
2498
|
+
}
|
|
2499
|
+
|
|
2441
2500
|
private static readonly EXAMPLE_GROUPS: Array<{
|
|
2442
2501
|
labelKey: string
|
|
2443
2502
|
itemKeys: string[]
|
|
2444
2503
|
}> = [
|
|
2445
2504
|
{
|
|
2446
|
-
labelKey: '
|
|
2505
|
+
labelKey: 'ai-assistant.label.create',
|
|
2447
2506
|
itemKeys: [
|
|
2448
|
-
'
|
|
2449
|
-
'
|
|
2507
|
+
'ai-assistant.example.create-monitoring-dashboard',
|
|
2508
|
+
'ai-assistant.example.create-welcome-screen'
|
|
2450
2509
|
]
|
|
2451
2510
|
},
|
|
2452
2511
|
{
|
|
2453
|
-
labelKey: '
|
|
2512
|
+
labelKey: 'ai-assistant.label.edit',
|
|
2454
2513
|
itemKeys: [
|
|
2455
|
-
'
|
|
2456
|
-
'
|
|
2514
|
+
'ai-assistant.example.edit-align-distribute',
|
|
2515
|
+
'ai-assistant.example.edit-resize-board'
|
|
2457
2516
|
]
|
|
2458
2517
|
},
|
|
2459
2518
|
{
|
|
2460
|
-
labelKey: '
|
|
2519
|
+
labelKey: 'ai-assistant.label.style',
|
|
2461
2520
|
itemKeys: [
|
|
2462
|
-
'
|
|
2463
|
-
'
|
|
2521
|
+
'ai-assistant.example.style-dark-mode',
|
|
2522
|
+
'ai-assistant.example.style-rounded-shadow'
|
|
2464
2523
|
]
|
|
2465
2524
|
}
|
|
2466
2525
|
]
|
|
@@ -2473,7 +2532,7 @@ export class OxBoardAIChat extends LitElement {
|
|
|
2473
2532
|
<div class="session-tabs" role="tablist">
|
|
2474
2533
|
${this.sessions.map((s, i) => {
|
|
2475
2534
|
const isActive = s.id === this.sessionId
|
|
2476
|
-
const fallback = i18next.t('
|
|
2535
|
+
const fallback = i18next.t('ai-assistant.text.session-tab-label', {
|
|
2477
2536
|
n: i + 1,
|
|
2478
2537
|
defaultValue: `세션 ${i + 1}`
|
|
2479
2538
|
})
|
|
@@ -2494,12 +2553,12 @@ export class OxBoardAIChat extends LitElement {
|
|
|
2494
2553
|
})}
|
|
2495
2554
|
<button
|
|
2496
2555
|
class="session-tab-new"
|
|
2497
|
-
title=${i18next.t('
|
|
2556
|
+
title=${i18next.t('ai-assistant.button.new-chat', {
|
|
2498
2557
|
defaultValue: '새 대화'
|
|
2499
2558
|
})}
|
|
2500
2559
|
@click=${this._onSessionCreateClick}>
|
|
2501
2560
|
<md-icon>add</md-icon>
|
|
2502
|
-
<span>${i18next.t('
|
|
2561
|
+
<span>${i18next.t('ai-assistant.button.new-chat', { defaultValue: '새 대화' })}</span>
|
|
2503
2562
|
</button>
|
|
2504
2563
|
</div>
|
|
2505
2564
|
`
|
|
@@ -2552,36 +2611,30 @@ export class OxBoardAIChat extends LitElement {
|
|
|
2552
2611
|
<div class="empty">
|
|
2553
2612
|
<div class="header">
|
|
2554
2613
|
<md-icon class="icon">auto_awesome</md-icon>
|
|
2555
|
-
<div class="title">${this.intro?.title ?? i18next.t('
|
|
2556
|
-
<div class="subtitle">${this.intro?.subtitle ?? i18next.t('
|
|
2614
|
+
<div class="title">${this.intro?.title ?? i18next.t('ai-assistant.text.empty-title')}</div>
|
|
2615
|
+
<div class="subtitle">${this.intro?.subtitle ?? i18next.t('ai-assistant.text.empty-subtitle')}</div>
|
|
2557
2616
|
</div>
|
|
2558
2617
|
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
@click=${() => this.insertTrigger('/')}>
|
|
2580
|
-
<span class="trigger-char">/</span>
|
|
2581
|
-
<span class="trigger-desc">${i18next.t('board-ai.label.trigger-insert')}</span>
|
|
2582
|
-
</button>
|
|
2583
|
-
</div>
|
|
2584
|
-
</div>
|
|
2618
|
+
${this.triggerChips.length > 0
|
|
2619
|
+
? html`
|
|
2620
|
+
<div class="trigger-row">
|
|
2621
|
+
<div class="trigger-row-label">${i18next.t('ai-assistant.text.triggers-heading')}</div>
|
|
2622
|
+
<div class="trigger-chips">
|
|
2623
|
+
${this.triggerChips.map(
|
|
2624
|
+
trigger => html`
|
|
2625
|
+
<button
|
|
2626
|
+
class="trigger-chip"
|
|
2627
|
+
title=${i18next.t(trigger.tooltipKey)}
|
|
2628
|
+
@click=${() => this.insertTrigger(trigger.char)}>
|
|
2629
|
+
<span class="trigger-char">${trigger.char}</span>
|
|
2630
|
+
<span class="trigger-desc">${i18next.t(trigger.labelKey)}</span>
|
|
2631
|
+
</button>
|
|
2632
|
+
`
|
|
2633
|
+
)}
|
|
2634
|
+
</div>
|
|
2635
|
+
</div>
|
|
2636
|
+
`
|
|
2637
|
+
: ''}
|
|
2585
2638
|
|
|
2586
2639
|
${this.exampleGroups.map(
|
|
2587
2640
|
group => html`
|
|
@@ -2599,11 +2652,12 @@ export class OxBoardAIChat extends LitElement {
|
|
|
2599
2652
|
)}
|
|
2600
2653
|
|
|
2601
2654
|
<div class="footer">
|
|
2602
|
-
<span class="badge">${i18next.t('
|
|
2603
|
-
<span class="badge">${i18next.t('
|
|
2604
|
-
<span class="badge">${i18next.t('
|
|
2605
|
-
|
|
2606
|
-
|
|
2655
|
+
<span class="badge">${i18next.t('ai-assistant.label.korean-supported')}</span>
|
|
2656
|
+
<span class="badge">${i18next.t('ai-assistant.label.multi-command')}</span>
|
|
2657
|
+
<span class="badge">${i18next.t('ai-assistant.label.review-able')}</span>
|
|
2658
|
+
${chatDefaults.footerNoticeKey
|
|
2659
|
+
? html`<br />${i18next.t(chatDefaults.footerNoticeKey)}`
|
|
2660
|
+
: ''}
|
|
2607
2661
|
</div>
|
|
2608
2662
|
</div>
|
|
2609
2663
|
`
|
|
@@ -2680,15 +2734,15 @@ export class OxBoardAIChat extends LitElement {
|
|
|
2680
2734
|
<button
|
|
2681
2735
|
class="msg-action ${isCopied ? 'confirmed' : ''}"
|
|
2682
2736
|
title=${isCopied
|
|
2683
|
-
? i18next.t('
|
|
2684
|
-
: i18next.t('
|
|
2737
|
+
? i18next.t('ai-assistant.text.copied')
|
|
2738
|
+
: i18next.t('ai-assistant.button.copy')}
|
|
2685
2739
|
@click=${() => this.copyMessage(idx, content)}>
|
|
2686
2740
|
<md-icon>${isCopied ? 'check' : 'content_copy'}</md-icon>
|
|
2687
2741
|
</button>
|
|
2688
2742
|
<button
|
|
2689
2743
|
class="msg-action"
|
|
2690
2744
|
?disabled=${this.busy}
|
|
2691
|
-
title=${i18next.t('
|
|
2745
|
+
title=${i18next.t('ai-assistant.button.regenerate')}
|
|
2692
2746
|
@click=${() => this.regenerateAssistant(idx)}>
|
|
2693
2747
|
<md-icon>refresh</md-icon>
|
|
2694
2748
|
</button>
|
|
@@ -2713,19 +2767,19 @@ export class OxBoardAIChat extends LitElement {
|
|
|
2713
2767
|
<md-icon>unfold_less</md-icon>
|
|
2714
2768
|
<span class="fn-text">
|
|
2715
2769
|
${folded.summarized
|
|
2716
|
-
? i18next.t('
|
|
2770
|
+
? i18next.t('ai-assistant.text.history-folded-summarized', {
|
|
2717
2771
|
count: folded.omitted,
|
|
2718
2772
|
defaultValue: '대화가 길어져 앞부분 {count}개가 요약으로 접혔습니다. 다른 주제라면 새 대화가 낫습니다.'
|
|
2719
2773
|
})
|
|
2720
|
-
: i18next.t('
|
|
2774
|
+
: i18next.t('ai-assistant.text.history-folded', {
|
|
2721
2775
|
count: folded.omitted,
|
|
2722
2776
|
defaultValue: '대화가 길어져 앞부분 {count}개가 생략되었습니다. 다른 주제라면 새 대화가 낫습니다.'
|
|
2723
2777
|
})}
|
|
2724
2778
|
</span>
|
|
2725
2779
|
<button class="fn-new" @click=${this.startNewFromNotice}>
|
|
2726
|
-
${i18next.t('
|
|
2780
|
+
${i18next.t('ai-assistant.button.new-chat')}
|
|
2727
2781
|
</button>
|
|
2728
|
-
<button class="fn-close" title=${i18next.t('
|
|
2782
|
+
<button class="fn-close" title=${i18next.t('ai-assistant.button.close')} @click=${() => (this.foldNoticeDismissed = true)}>
|
|
2729
2783
|
<md-icon>close</md-icon>
|
|
2730
2784
|
</button>
|
|
2731
2785
|
</div>
|
|
@@ -2767,8 +2821,8 @@ export class OxBoardAIChat extends LitElement {
|
|
|
2767
2821
|
class="run"
|
|
2768
2822
|
?disabled=${sent}
|
|
2769
2823
|
@click=${() => this.executeProposal(p, key)}>
|
|
2770
|
-
${sent ? i18next.t('
|
|
2771
|
-
: i18next.t('
|
|
2824
|
+
${sent ? i18next.t('ai-assistant.text.proposal-sent', { defaultValue: '실행 요청됨' })
|
|
2825
|
+
: i18next.t('ai-assistant.button.run-proposal', { defaultValue: '실행' })}
|
|
2772
2826
|
</button>
|
|
2773
2827
|
</div>
|
|
2774
2828
|
`
|
|
@@ -2829,7 +2883,7 @@ export class OxBoardAIChat extends LitElement {
|
|
|
2829
2883
|
<div class="grounding-warning" role="note">
|
|
2830
2884
|
<md-icon>report</md-icon>
|
|
2831
2885
|
<span>
|
|
2832
|
-
${i18next.t('
|
|
2886
|
+
${i18next.t('ai-assistant.text.ungrounded-mention', {
|
|
2833
2887
|
defaultValue: '확인되지 않은 대상을 언급했습니다 — 실제 데이터에 없습니다:'
|
|
2834
2888
|
})}
|
|
2835
2889
|
<span class="ids">${ids.join(', ')}</span>
|
|
@@ -2851,7 +2905,7 @@ export class OxBoardAIChat extends LitElement {
|
|
|
2851
2905
|
const open = !!line.toolUsagesOpen
|
|
2852
2906
|
const readCount = usages.filter(u => u.kind === 'read').length
|
|
2853
2907
|
const writeCount = usages.filter(u => u.kind === 'write').length
|
|
2854
|
-
const toolsLabel = i18next.t('
|
|
2908
|
+
const toolsLabel = i18next.t('ai-assistant.text.tools-used', { defaultValue: '도구 사용' })
|
|
2855
2909
|
/* 눈에 걸려야 하는 것을 머리줄에 올린다 — 거절·접힘이 있으면 펼치기 전에 보인다.
|
|
2856
2910
|
* (이번 사고들이 전부 그것이었다: 인자 빠진 호출이 거절됐고, 중복 제안이 접혔다.) */
|
|
2857
2911
|
const problems = usages.filter(u => u.outcome === 'rejected' || u.outcome === 'error').length
|
|
@@ -2867,12 +2921,12 @@ export class OxBoardAIChat extends LitElement {
|
|
|
2867
2921
|
<strong>${usages.length}</strong> ${toolsLabel}
|
|
2868
2922
|
<span class="tool-usages-counts">
|
|
2869
2923
|
· read ${readCount} · write ${writeCount}${problems
|
|
2870
|
-
? html` · <span class="tu-flag bad">${i18next.t('
|
|
2924
|
+
? html` · <span class="tu-flag bad">${i18next.t('ai-assistant.text.tool-rejected-count', {
|
|
2871
2925
|
count: problems,
|
|
2872
2926
|
defaultValue: '거절 {count}'
|
|
2873
2927
|
})}</span>`
|
|
2874
2928
|
: nothing}${folded
|
|
2875
|
-
? html` · <span class="tu-flag">${i18next.t('
|
|
2929
|
+
? html` · <span class="tu-flag">${i18next.t('ai-assistant.text.tool-folded-count', {
|
|
2876
2930
|
count: folded,
|
|
2877
2931
|
defaultValue: '접힘 {count}'
|
|
2878
2932
|
})}</span>`
|
|
@@ -2916,7 +2970,7 @@ export class OxBoardAIChat extends LitElement {
|
|
|
2916
2970
|
return html`
|
|
2917
2971
|
${newTurn
|
|
2918
2972
|
? html`<li class="tu-turn">
|
|
2919
|
-
${i18next.t('
|
|
2973
|
+
${i18next.t('ai-assistant.text.tool-turn', { n: (u.iter ?? 0) + 1, defaultValue: '{n}번째 판단' })}
|
|
2920
2974
|
</li>`
|
|
2921
2975
|
: nothing}
|
|
2922
2976
|
<li class="tool-usage-item kind-${u.kind} outcome-${outcome}">
|
|
@@ -2942,12 +2996,12 @@ export class OxBoardAIChat extends LitElement {
|
|
|
2942
2996
|
/** 배지 문구 — 코드가 아니라 사람 말로. */
|
|
2943
2997
|
private outcomeLabel(outcome: string): string {
|
|
2944
2998
|
const map: Record<string, [string, string]> = {
|
|
2945
|
-
ok: ['
|
|
2946
|
-
rejected: ['
|
|
2947
|
-
queued: ['
|
|
2948
|
-
proposed: ['
|
|
2949
|
-
folded: ['
|
|
2950
|
-
error: ['
|
|
2999
|
+
ok: ['ai-assistant.text.outcome-ok', '조회'],
|
|
3000
|
+
rejected: ['ai-assistant.text.outcome-rejected', '거절'],
|
|
3001
|
+
queued: ['ai-assistant.text.outcome-queued', '적용 예정'],
|
|
3002
|
+
proposed: ['ai-assistant.text.outcome-proposed', '제안'],
|
|
3003
|
+
folded: ['ai-assistant.text.outcome-folded', '중복 접힘'],
|
|
3004
|
+
error: ['ai-assistant.text.outcome-error', '오류']
|
|
2951
3005
|
}
|
|
2952
3006
|
const [key, fallback] = map[outcome] ?? map.ok
|
|
2953
3007
|
return i18next.t(key, { defaultValue: fallback })
|
|
@@ -3008,7 +3062,7 @@ export class OxBoardAIChat extends LitElement {
|
|
|
3008
3062
|
// 클립보드에는 marker 제거된 깨끗한 텍스트
|
|
3009
3063
|
await navigator.clipboard.writeText(stripMentionRefids(content))
|
|
3010
3064
|
this.copiedIdx = idx
|
|
3011
|
-
this.showToast(i18next.t('
|
|
3065
|
+
this.showToast(i18next.t('ai-assistant.text.copied'))
|
|
3012
3066
|
setTimeout(() => {
|
|
3013
3067
|
if (this.copiedIdx === idx) this.copiedIdx = undefined
|
|
3014
3068
|
}, 1500)
|
|
@@ -3128,30 +3182,41 @@ export class OxBoardAIChat extends LitElement {
|
|
|
3128
3182
|
const liveBoard = this.boardProvider ? this.boardProvider() : this.currentBoard
|
|
3129
3183
|
|
|
3130
3184
|
// mentions 는 위에서 이미 계산해 user line 에 thread — 그대로 mutation 입력으로 재사용.
|
|
3185
|
+
const neutral = this.chatEndpoint === 'assistantChat'
|
|
3131
3186
|
const result = await client.mutate({
|
|
3132
|
-
mutation: BOARD_AI_CHAT_MUTATION,
|
|
3187
|
+
mutation: neutral ? ASSISTANT_CHAT_MUTATION : BOARD_AI_CHAT_MUTATION,
|
|
3133
3188
|
variables: {
|
|
3134
|
-
input:
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3189
|
+
input: neutral
|
|
3190
|
+
? buildAssistantChatInput({
|
|
3191
|
+
sessionId: this.sessionId,
|
|
3192
|
+
history,
|
|
3193
|
+
systemPrompt: this.systemPrompt!,
|
|
3194
|
+
truncateAfterMessageId: this._truncateAfterId,
|
|
3195
|
+
hostContext: this.hostContext,
|
|
3196
|
+
toolCategories: this.toolCategories,
|
|
3197
|
+
requireGroundingTools: this.requireGroundingTools
|
|
3198
|
+
})
|
|
3199
|
+
: buildChatMutationInput({
|
|
3200
|
+
sessionId: this.sessionId,
|
|
3201
|
+
history,
|
|
3202
|
+
liveBoard,
|
|
3203
|
+
scopes: this.scopes,
|
|
3204
|
+
truncateAfterMessageId: this._truncateAfterId,
|
|
3205
|
+
hostContext: this.hostContext,
|
|
3206
|
+
toolCategories: this.toolCategories,
|
|
3207
|
+
boardTools: this.boardTools,
|
|
3208
|
+
requireGroundingTools: this.requireGroundingTools,
|
|
3209
|
+
knownTypes: this.knownTypes,
|
|
3210
|
+
categories: this.categories,
|
|
3211
|
+
componentSchemas: this.componentSchemas,
|
|
3212
|
+
selectedRefids: this.selectedRefids ?? [],
|
|
3213
|
+
mentions
|
|
3214
|
+
})
|
|
3150
3215
|
}
|
|
3151
3216
|
})
|
|
3152
3217
|
|
|
3153
|
-
const out = result.data?.boardAIChat
|
|
3154
|
-
if (!out) throw new Error(i18next.t('
|
|
3218
|
+
const out = neutral ? result.data?.assistantChat : result.data?.boardAIChat
|
|
3219
|
+
if (!out) throw new Error(i18next.t('ai-assistant.text.empty-response'))
|
|
3155
3220
|
|
|
3156
3221
|
/* 대화가 길어졌다는 서버 보고 — 닫지 않았다면 안내를 띄운다. */
|
|
3157
3222
|
this.historyFolded = out.historyFolded ?? undefined
|
|
@@ -16,6 +16,18 @@
|
|
|
16
16
|
export interface ChatDefaults {
|
|
17
17
|
catalogEntries: any[]
|
|
18
18
|
slashTemplates: any[]
|
|
19
|
+
/**
|
|
20
|
+
* 빈 화면 맨 아래 한 줄의 **i18n 키**. 없으면 그 줄을 안 그린다.
|
|
21
|
+
*
|
|
22
|
+
* 문장이 아니라 키를 받는 이유: 호스트의 client barrel 은 모듈 로드 시점에 돌고, 그때 i18next 가
|
|
23
|
+
* 아직 언어를 안 잡았을 수 있다. 키로 받아 **그릴 때** 번역한다.
|
|
24
|
+
*
|
|
25
|
+
* 왜 호스트 것인가: 여기 있던 기본 문장이 「AI 응답은 항상 **보드에** 적용 전 미리보기로 확인
|
|
26
|
+
* 가능합니다」였다. 보드가 없는 제품에서는 거짓이고(figure 에서 실제로 그렇게 떴다), 그 약속은
|
|
27
|
+
* 보드 저작기가 patch 를 미리보기로 거는 **기계가 있어서** 할 수 있는 말이다. 기계를 가진 쪽이
|
|
28
|
+
* 그 말을 한다.
|
|
29
|
+
*/
|
|
30
|
+
footerNoticeKey?: string
|
|
19
31
|
}
|
|
20
32
|
|
|
21
33
|
export const chatDefaults: ChatDefaults = { catalogEntries: [], slashTemplates: [] }
|
|
@@ -24,4 +36,5 @@ export const chatDefaults: ChatDefaults = { catalogEntries: [], slashTemplates:
|
|
|
24
36
|
export function registerChatDefaults(defaults: Partial<ChatDefaults>): void {
|
|
25
37
|
if (defaults.catalogEntries) chatDefaults.catalogEntries = defaults.catalogEntries
|
|
26
38
|
if (defaults.slashTemplates) chatDefaults.slashTemplates = defaults.slashTemplates
|
|
39
|
+
if (defaults.footerNoticeKey) chatDefaults.footerNoticeKey = defaults.footerNoticeKey
|
|
27
40
|
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* What this guards: **the two doors take different inputs, and neither one leaks into the other.**
|
|
3
|
+
*
|
|
4
|
+
* The chat component talks to one of two server operations. `boardAIChat` is board authoring and
|
|
5
|
+
* carries the board model; `assistantChat` is for a surface with no document of its own. Sending
|
|
6
|
+
* a board field to the neutral door, or dropping a required one, produces a GraphQL error the
|
|
7
|
+
* user sees as "the assistant is broken".
|
|
8
|
+
*
|
|
9
|
+
* The required system prompt is the one that would be silent. A conversation with no persona
|
|
10
|
+
* still answers — as nobody, on a surface that never said what it is — and that reads as a poor
|
|
11
|
+
* model rather than a missing argument.
|
|
12
|
+
*/
|
|
13
|
+
import { buildAssistantChatInput, buildChatMutationInput } from './chat-input-builder'
|
|
14
|
+
|
|
15
|
+
const history = [{ role: 'user', content: 'what figures do we have' }]
|
|
16
|
+
|
|
17
|
+
const args = (over: any = {}) => ({ history, systemPrompt: 'you help someone draw a figure', ...over })
|
|
18
|
+
|
|
19
|
+
describe('the neutral door carries no document', () => {
|
|
20
|
+
it('★ no board field reaches assistantChat', () => {
|
|
21
|
+
const input = buildAssistantChatInput(args({ sessionId: 's-1' }))
|
|
22
|
+
|
|
23
|
+
for (const field of ['currentBoard', 'selectedRefids', 'boardTools', 'knownTypes', 'componentSchemas', 'scopes', 'mentions']) {
|
|
24
|
+
expect(input).not.toHaveProperty(field)
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('carries the conversation, the session and the persona', () => {
|
|
29
|
+
const input = buildAssistantChatInput(args({ sessionId: 's-1' }))
|
|
30
|
+
|
|
31
|
+
expect(input.sessionId).toBe('s-1')
|
|
32
|
+
expect(input.messages).toBe(history)
|
|
33
|
+
expect(input.systemPrompt).toBe('you help someone draw a figure')
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('a one-off question has no session — null, not absent, so the server reads it as ad-hoc', () => {
|
|
37
|
+
expect(buildAssistantChatInput(args()).sessionId).toBeNull()
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
describe('the system prompt is required', () => {
|
|
42
|
+
it('★ refuses an empty prompt instead of sending one', () => {
|
|
43
|
+
expect(() => buildAssistantChatInput(args({ systemPrompt: '' }))).toThrow(/systemPrompt is required/)
|
|
44
|
+
expect(() => buildAssistantChatInput(args({ systemPrompt: ' ' }))).toThrow(/systemPrompt is required/)
|
|
45
|
+
expect(() => buildAssistantChatInput(args({ systemPrompt: undefined }))).toThrow(/systemPrompt is required/)
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
describe('optional fields are sent only when they mean something', () => {
|
|
50
|
+
it('an unset tool category list is left out — the server default is every registered category', () => {
|
|
51
|
+
expect(buildAssistantChatInput(args())).not.toHaveProperty('toolCategories')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('★ an empty tool category list is sent — it means none, which is a different answer', () => {
|
|
55
|
+
expect(buildAssistantChatInput(args({ toolCategories: [] })).toolCategories).toEqual([])
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('grounding is sent only when required — false is the server default', () => {
|
|
59
|
+
expect(buildAssistantChatInput(args())).not.toHaveProperty('requireGroundingTools')
|
|
60
|
+
expect(buildAssistantChatInput(args({ requireGroundingTools: true })).requireGroundingTools).toBe(true)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('host context and truncation travel when given', () => {
|
|
64
|
+
const input = buildAssistantChatInput(args({ hostContext: { figureId: 'fig-1' }, truncateAfterMessageId: 'm-9' }))
|
|
65
|
+
|
|
66
|
+
expect(input.hostContext).toEqual({ figureId: 'fig-1' })
|
|
67
|
+
expect(input.truncateAfterMessageId).toBe('m-9')
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
describe('the board door is unchanged', () => {
|
|
72
|
+
it('★ still carries selectedRefids — a missing one was why the AI did not know what was selected', () => {
|
|
73
|
+
const input = buildChatMutationInput({ history, liveBoard: null, selectedRefids: [3, 7] } as any)
|
|
74
|
+
expect(input.selectedRefids).toEqual([3, 7])
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('does not take a system prompt — the board conversation has its persona on the server', () => {
|
|
78
|
+
const input = buildChatMutationInput({ history, liveBoard: null, selectedRefids: [] } as any)
|
|
79
|
+
expect(input).not.toHaveProperty('systemPrompt')
|
|
80
|
+
})
|
|
81
|
+
})
|