@things-factory/ai-assistant 10.1.6 → 10.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
export interface ChatDefaults {
|
|
3
3
|
catalogEntries: any[];
|
|
4
4
|
slashTemplates: any[];
|
|
5
|
+
/**
|
|
6
|
+
* 빈 화면 맨 아래 한 줄의 **i18n 키**. 없으면 그 줄을 안 그린다.
|
|
7
|
+
*
|
|
8
|
+
* 문장이 아니라 키를 받는 이유: 호스트의 client barrel 은 모듈 로드 시점에 돌고, 그때 i18next 가
|
|
9
|
+
* 아직 언어를 안 잡았을 수 있다. 키로 받아 **그릴 때** 번역한다.
|
|
10
|
+
*
|
|
11
|
+
* 왜 호스트 것인가: 여기 있던 기본 문장이 「AI 응답은 항상 **보드에** 적용 전 미리보기로 확인
|
|
12
|
+
* 가능합니다」였다. 보드가 없는 제품에서는 거짓이고(figure 에서 실제로 그렇게 떴다), 그 약속은
|
|
13
|
+
* 보드 저작기가 patch 를 미리보기로 거는 **기계가 있어서** 할 수 있는 말이다. 기계를 가진 쪽이
|
|
14
|
+
* 그 말을 한다.
|
|
15
|
+
*/
|
|
16
|
+
footerNoticeKey?: string;
|
|
5
17
|
}
|
|
6
18
|
export declare const chatDefaults: ChatDefaults;
|
|
7
19
|
/** 호스트가 자기 기본 목록을 넣는다. 두 번 부르면 뒤엣것이 이긴다(같은 호스트의 재로드). */
|
|
@@ -18,5 +18,7 @@ export function registerChatDefaults(defaults) {
|
|
|
18
18
|
chatDefaults.catalogEntries = defaults.catalogEntries;
|
|
19
19
|
if (defaults.slashTemplates)
|
|
20
20
|
chatDefaults.slashTemplates = defaults.slashTemplates;
|
|
21
|
+
if (defaults.footerNoticeKey)
|
|
22
|
+
chatDefaults.footerNoticeKey = defaults.footerNoticeKey;
|
|
21
23
|
}
|
|
22
24
|
//# sourceMappingURL=chat-defaults.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chat-defaults.js","sourceRoot":"","sources":["../../client/components/chat-defaults.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;
|
|
1
|
+
{"version":3,"file":"chat-defaults.js","sourceRoot":"","sources":["../../client/components/chat-defaults.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAoBH,MAAM,CAAC,MAAM,YAAY,GAAiB,EAAE,cAAc,EAAE,EAAE,EAAE,cAAc,EAAE,EAAE,EAAE,CAAA;AAEpF,yDAAyD;AACzD,MAAM,UAAU,oBAAoB,CAAC,QAA+B;IAClE,IAAI,QAAQ,CAAC,cAAc;QAAE,YAAY,CAAC,cAAc,GAAG,QAAQ,CAAC,cAAc,CAAA;IAClF,IAAI,QAAQ,CAAC,cAAc;QAAE,YAAY,CAAC,cAAc,GAAG,QAAQ,CAAC,cAAc,CAAA;IAClF,IAAI,QAAQ,CAAC,eAAe;QAAE,YAAY,CAAC,eAAe,GAAG,QAAQ,CAAC,eAAe,CAAA;AACvF,CAAC","sourcesContent":["/*\n * 대화면의 기본 목록 — **넣는 쪽이 주인이다.**\n *\n * `@` 카탈로그와 `/` 슬래시 목록은 도메인마다 다르다. 보드 저작에는 컴포넌트 타입이 뜨고, 공장\n * 대화면에는 그것이 뜨면 안 된다. 그런데 대화면이 보드 목록을 직접 import 하고 있어서, 보드가\n * 없는 제품이 이 컴포넌트를 쓰면 **쓸 수 없는 항목이 목록에 떴다.**\n *\n * 그래서 호스트가 넣는다. `board-ai` 를 import 하면 그 패키지가 부팅 때 자기 것을 넣고(client\n * barrel), 안 하는 제품은 빈 채로 시작한다. 빈 것이 맞다 — 없는 것을 보여 주는 것보다 낫다.\n *\n * ⚠ 대화면이 뜬 뒤에 넣으면 늦는다. 후보 캐시가 인스턴스 만들 때 한 번 만들어진다. 호스트의\n * client barrel(=모듈 로드 시점)에서 부르면 항상 먼저다.\n */\n\n/** 목록의 모양은 이 파일이 정하지 않는다 — 넣는 쪽의 타입을 그대로 받는다. */\nexport interface ChatDefaults {\n catalogEntries: any[]\n slashTemplates: any[]\n /**\n * 빈 화면 맨 아래 한 줄의 **i18n 키**. 없으면 그 줄을 안 그린다.\n *\n * 문장이 아니라 키를 받는 이유: 호스트의 client barrel 은 모듈 로드 시점에 돌고, 그때 i18next 가\n * 아직 언어를 안 잡았을 수 있다. 키로 받아 **그릴 때** 번역한다.\n *\n * 왜 호스트 것인가: 여기 있던 기본 문장이 「AI 응답은 항상 **보드에** 적용 전 미리보기로 확인\n * 가능합니다」였다. 보드가 없는 제품에서는 거짓이고(figure 에서 실제로 그렇게 떴다), 그 약속은\n * 보드 저작기가 patch 를 미리보기로 거는 **기계가 있어서** 할 수 있는 말이다. 기계를 가진 쪽이\n * 그 말을 한다.\n */\n footerNoticeKey?: string\n}\n\nexport const chatDefaults: ChatDefaults = { catalogEntries: [], slashTemplates: [] }\n\n/** 호스트가 자기 기본 목록을 넣는다. 두 번 부르면 뒤엣것이 이긴다(같은 호스트의 재로드). */\nexport function registerChatDefaults(defaults: Partial<ChatDefaults>): void {\n if (defaults.catalogEntries) chatDefaults.catalogEntries = defaults.catalogEntries\n if (defaults.slashTemplates) chatDefaults.slashTemplates = defaults.slashTemplates\n if (defaults.footerNoticeKey) chatDefaults.footerNoticeKey = defaults.footerNoticeKey\n}\n"]}
|
|
@@ -53,3 +53,32 @@ export interface ChatMutationInputArgs {
|
|
|
53
53
|
* "AI 가 선택된 컴포넌트를 모른다" 의 root cause 였다.
|
|
54
54
|
*/
|
|
55
55
|
export declare function buildChatMutationInput(args: ChatMutationInputArgs): Record<string, any>;
|
|
56
|
+
/**
|
|
57
|
+
* Input for `assistantChat` — the door for a surface that has no document of its own.
|
|
58
|
+
*
|
|
59
|
+
* Deliberately a separate builder rather than a flag on the one above. The two mutations take
|
|
60
|
+
* different inputs, and folding them together would mean a board field reaching a surface that
|
|
61
|
+
* has no board, or a required field going missing with nothing to notice it. The one thing they
|
|
62
|
+
* share is the conversation, and that is passed the same way.
|
|
63
|
+
*/
|
|
64
|
+
export interface AssistantChatInputArgs {
|
|
65
|
+
sessionId?: string;
|
|
66
|
+
history: Array<{
|
|
67
|
+
role: string;
|
|
68
|
+
content: string;
|
|
69
|
+
}>;
|
|
70
|
+
/** Who the assistant is on this surface. The host owns this prose; the server appends tool rules. */
|
|
71
|
+
systemPrompt: string;
|
|
72
|
+
truncateAfterMessageId?: string;
|
|
73
|
+
hostContext?: any;
|
|
74
|
+
toolCategories?: string[];
|
|
75
|
+
requireGroundingTools?: boolean;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Build the neutral input.
|
|
79
|
+
*
|
|
80
|
+
* Refuses an empty system prompt instead of sending one. A conversation with no persona still
|
|
81
|
+
* answers — it just answers as nobody, on a surface that never said what it is — and that reads
|
|
82
|
+
* as a bad model rather than a missing argument.
|
|
83
|
+
*/
|
|
84
|
+
export declare function buildAssistantChatInput(args: AssistantChatInputArgs): Record<string, any>;
|
|
@@ -29,4 +29,26 @@ export function buildChatMutationInput(args) {
|
|
|
29
29
|
...(args.mentions && args.mentions.length > 0 && { mentions: args.mentions })
|
|
30
30
|
};
|
|
31
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* Build the neutral input.
|
|
34
|
+
*
|
|
35
|
+
* Refuses an empty system prompt instead of sending one. A conversation with no persona still
|
|
36
|
+
* answers — it just answers as nobody, on a surface that never said what it is — and that reads
|
|
37
|
+
* as a bad model rather than a missing argument.
|
|
38
|
+
*/
|
|
39
|
+
export function buildAssistantChatInput(args) {
|
|
40
|
+
if (!args.systemPrompt?.trim()) {
|
|
41
|
+
throw new Error('[ox-board-ai-chat] systemPrompt is required when the chat talks to assistantChat');
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
sessionId: args.sessionId ?? null,
|
|
45
|
+
messages: args.history,
|
|
46
|
+
systemPrompt: args.systemPrompt,
|
|
47
|
+
...(args.truncateAfterMessageId ? { truncateAfterMessageId: args.truncateAfterMessageId } : {}),
|
|
48
|
+
...(args.hostContext ? { hostContext: args.hostContext } : {}),
|
|
49
|
+
/* Absent means every registered category; an empty array means none. Both are meant, so pass it through. */
|
|
50
|
+
...(args.toolCategories ? { toolCategories: args.toolCategories } : {}),
|
|
51
|
+
...(args.requireGroundingTools ? { requireGroundingTools: true } : {})
|
|
52
|
+
};
|
|
53
|
+
}
|
|
32
54
|
//# sourceMappingURL=chat-input-builder.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chat-input-builder.js","sourceRoot":"","sources":["../../client/components/chat-input-builder.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAuCH;;;;;GAKG;AACH,MAAM,UAAU,sBAAsB,CAAC,IAA2B;IAChE,OAAO;QACL,SAAS,EAAE,IAAI,CAAC,SAAS,IAAI,IAAI;QACjC,QAAQ,EAAE,IAAI,CAAC,OAAO;QACtB,YAAY,EAAE,IAAI,CAAC,SAAS,IAAI,IAAI;QACpC,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,GAAG,CAAC,IAAI,CAAC,sBAAsB,CAAC,CAAC,CAAC,EAAE,sBAAsB,EAAE,IAAI,CAAC,sBAAsB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/F,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC9D,qDAAqD;QACrD,GAAG,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,IAAI,CAAC,cAAc,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACvE,GAAG,CAAC,IAAI,CAAC,UAAU,KAAK,KAAK,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC3D,GAAG,CAAC,IAAI,CAAC,qBAAqB,CAAC,CAAC,CAAC,EAAE,qBAAqB,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACtE,UAAU,EAAE,IAAI,CAAC,UAAU;QAC3B,UAAU,EAAE,IAAI,CAAC,UAAU;QAC3B,gBAAgB,EAAE,IAAI,CAAC,gBAAgB;QACvC,cAAc,EAAE,IAAI,CAAC,cAAc;QACnC,GAAG,CAAC,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,CAAC;KAC9E,CAAA;AACH,CAAC","sourcesContent":["/**\n * boardAIChat GraphQL mutation 의 input 객체 빌더.\n *\n * board-ai-chat.ts (Lit / Material Web 의존) 와 분리 — pure 함수만 두면 jest 의 node\n * 환경에서도 import 가능. 회귀 테스트 (selectedRefids 누락 등) 가 단위 테스트로 잠긴다.\n */\n\nexport interface ChatMutationInputArgs {\n sessionId?: string\n history: Array<{ role: string; content: string }>\n liveBoard: any\n scopes?: string[]\n /** 편집으로 접은 지점(마지막으로 남긴 서버 메시지 id) — 서버가 그 뒤 이력을 에이전트 문맥에서 뺀다. */\n truncateAfterMessageId?: string\n /** 이 대화면이 무엇을 보고 있는지(예: { spaceId }) — 도구가 대상을 알 수 있게. 없으면 모델이 식별자를 되묻는다. */\n hostContext?: any\n /**\n * 이 대화면이 쓸 수 있는 외부 도구 카테고리(registerToolCategory 이름).\n * 미지정 = 등록된 전부(현행 동작). 지정하면 정의와 **실행** 모두 그 범위로 제한된다.\n */\n toolCategories?: string[]\n /** 코어 보드 편집 도구(addComponent·스타일링·씬 조작) 사용 여부. 기본 true. */\n boardTools?: boolean\n /**\n * 첫 턴에 도구 호출을 강제할지 — 라이브 상태를 다루는 대화면에서 켠다.\n * 근거 없이 상태를 단언하는 답(도구를 아예 부르지 않은 답)을 원천에서 막는다.\n */\n requireGroundingTools?: boolean\n knownTypes?: string[]\n categories?: string[]\n componentSchemas?: any\n /**\n * 선택된 컴포넌트의 `refid` 배열 — things-scene universal numeric handle.\n * id (데이터 바인딩 이름) 와는 다른 개념.\n */\n selectedRefids: number[]\n /**\n * popup 에서 명시 선택한 #mention 매핑 — {token, refid}.\n * 사용자 textarea 에는 #token 만 보이고, 정확한 refid 는 이 별도 채널로 server 에 전달.\n * server 가 mention map 으로 LLM 에 정확한 refid 정보 첨부 (사용자 노출 0).\n */\n mentions?: Array<{ token: string; refid: number }>\n}\n\n/**\n * GraphQL mutation 의 input 객체를 빌드.\n *\n * 핵심 보호 — `selectedRefids` 가 항상 포함되어야 한다. 누락이 사용자에게 보고된\n * \"AI 가 선택된 컴포넌트를 모른다\" 의 root cause 였다.\n */\nexport function buildChatMutationInput(args: ChatMutationInputArgs): Record<string, any> {\n return {\n sessionId: args.sessionId ?? null,\n messages: args.history,\n currentBoard: args.liveBoard ?? null,\n scopes: args.scopes,\n ...(args.truncateAfterMessageId ? { truncateAfterMessageId: args.truncateAfterMessageId } : {}),\n ...(args.hostContext ? { hostContext: args.hostContext } : {}),\n /* 미지정은 서버 기본(전부 허용)에 맡긴다 — null 을 보내 굳이 덮어쓰지 않는다. */\n ...(args.toolCategories ? { toolCategories: args.toolCategories } : {}),\n ...(args.boardTools === false ? { boardTools: false } : {}),\n ...(args.requireGroundingTools ? { requireGroundingTools: true } : {}),\n knownTypes: args.knownTypes,\n categories: args.categories,\n componentSchemas: args.componentSchemas,\n selectedRefids: args.selectedRefids,\n ...(args.mentions && args.mentions.length > 0 && { mentions: args.mentions })\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"chat-input-builder.js","sourceRoot":"","sources":["../../client/components/chat-input-builder.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAuCH;;;;;GAKG;AACH,MAAM,UAAU,sBAAsB,CAAC,IAA2B;IAChE,OAAO;QACL,SAAS,EAAE,IAAI,CAAC,SAAS,IAAI,IAAI;QACjC,QAAQ,EAAE,IAAI,CAAC,OAAO;QACtB,YAAY,EAAE,IAAI,CAAC,SAAS,IAAI,IAAI;QACpC,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,GAAG,CAAC,IAAI,CAAC,sBAAsB,CAAC,CAAC,CAAC,EAAE,sBAAsB,EAAE,IAAI,CAAC,sBAAsB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/F,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC9D,qDAAqD;QACrD,GAAG,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,IAAI,CAAC,cAAc,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACvE,GAAG,CAAC,IAAI,CAAC,UAAU,KAAK,KAAK,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC3D,GAAG,CAAC,IAAI,CAAC,qBAAqB,CAAC,CAAC,CAAC,EAAE,qBAAqB,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACtE,UAAU,EAAE,IAAI,CAAC,UAAU;QAC3B,UAAU,EAAE,IAAI,CAAC,UAAU;QAC3B,gBAAgB,EAAE,IAAI,CAAC,gBAAgB;QACvC,cAAc,EAAE,IAAI,CAAC,cAAc;QACnC,GAAG,CAAC,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,CAAC;KAC9E,CAAA;AACH,CAAC;AAqBD;;;;;;GAMG;AACH,MAAM,UAAU,uBAAuB,CAAC,IAA4B;IAClE,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,IAAI,EAAE,EAAE,CAAC;QAC/B,MAAM,IAAI,KAAK,CAAC,kFAAkF,CAAC,CAAA;IACrG,CAAC;IAED,OAAO;QACL,SAAS,EAAE,IAAI,CAAC,SAAS,IAAI,IAAI;QACjC,QAAQ,EAAE,IAAI,CAAC,OAAO;QACtB,YAAY,EAAE,IAAI,CAAC,YAAY;QAC/B,GAAG,CAAC,IAAI,CAAC,sBAAsB,CAAC,CAAC,CAAC,EAAE,sBAAsB,EAAE,IAAI,CAAC,sBAAsB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/F,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC9D,4GAA4G;QAC5G,GAAG,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,IAAI,CAAC,cAAc,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACvE,GAAG,CAAC,IAAI,CAAC,qBAAqB,CAAC,CAAC,CAAC,EAAE,qBAAqB,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACvE,CAAA;AACH,CAAC","sourcesContent":["/**\n * boardAIChat GraphQL mutation 의 input 객체 빌더.\n *\n * board-ai-chat.ts (Lit / Material Web 의존) 와 분리 — pure 함수만 두면 jest 의 node\n * 환경에서도 import 가능. 회귀 테스트 (selectedRefids 누락 등) 가 단위 테스트로 잠긴다.\n */\n\nexport interface ChatMutationInputArgs {\n sessionId?: string\n history: Array<{ role: string; content: string }>\n liveBoard: any\n scopes?: string[]\n /** 편집으로 접은 지점(마지막으로 남긴 서버 메시지 id) — 서버가 그 뒤 이력을 에이전트 문맥에서 뺀다. */\n truncateAfterMessageId?: string\n /** 이 대화면이 무엇을 보고 있는지(예: { spaceId }) — 도구가 대상을 알 수 있게. 없으면 모델이 식별자를 되묻는다. */\n hostContext?: any\n /**\n * 이 대화면이 쓸 수 있는 외부 도구 카테고리(registerToolCategory 이름).\n * 미지정 = 등록된 전부(현행 동작). 지정하면 정의와 **실행** 모두 그 범위로 제한된다.\n */\n toolCategories?: string[]\n /** 코어 보드 편집 도구(addComponent·스타일링·씬 조작) 사용 여부. 기본 true. */\n boardTools?: boolean\n /**\n * 첫 턴에 도구 호출을 강제할지 — 라이브 상태를 다루는 대화면에서 켠다.\n * 근거 없이 상태를 단언하는 답(도구를 아예 부르지 않은 답)을 원천에서 막는다.\n */\n requireGroundingTools?: boolean\n knownTypes?: string[]\n categories?: string[]\n componentSchemas?: any\n /**\n * 선택된 컴포넌트의 `refid` 배열 — things-scene universal numeric handle.\n * id (데이터 바인딩 이름) 와는 다른 개념.\n */\n selectedRefids: number[]\n /**\n * popup 에서 명시 선택한 #mention 매핑 — {token, refid}.\n * 사용자 textarea 에는 #token 만 보이고, 정확한 refid 는 이 별도 채널로 server 에 전달.\n * server 가 mention map 으로 LLM 에 정확한 refid 정보 첨부 (사용자 노출 0).\n */\n mentions?: Array<{ token: string; refid: number }>\n}\n\n/**\n * GraphQL mutation 의 input 객체를 빌드.\n *\n * 핵심 보호 — `selectedRefids` 가 항상 포함되어야 한다. 누락이 사용자에게 보고된\n * \"AI 가 선택된 컴포넌트를 모른다\" 의 root cause 였다.\n */\nexport function buildChatMutationInput(args: ChatMutationInputArgs): Record<string, any> {\n return {\n sessionId: args.sessionId ?? null,\n messages: args.history,\n currentBoard: args.liveBoard ?? null,\n scopes: args.scopes,\n ...(args.truncateAfterMessageId ? { truncateAfterMessageId: args.truncateAfterMessageId } : {}),\n ...(args.hostContext ? { hostContext: args.hostContext } : {}),\n /* 미지정은 서버 기본(전부 허용)에 맡긴다 — null 을 보내 굳이 덮어쓰지 않는다. */\n ...(args.toolCategories ? { toolCategories: args.toolCategories } : {}),\n ...(args.boardTools === false ? { boardTools: false } : {}),\n ...(args.requireGroundingTools ? { requireGroundingTools: true } : {}),\n knownTypes: args.knownTypes,\n categories: args.categories,\n componentSchemas: args.componentSchemas,\n selectedRefids: args.selectedRefids,\n ...(args.mentions && args.mentions.length > 0 && { mentions: args.mentions })\n }\n}\n\n/**\n * Input for `assistantChat` — the door for a surface that has no document of its own.\n *\n * Deliberately a separate builder rather than a flag on the one above. The two mutations take\n * different inputs, and folding them together would mean a board field reaching a surface that\n * has no board, or a required field going missing with nothing to notice it. The one thing they\n * share is the conversation, and that is passed the same way.\n */\nexport interface AssistantChatInputArgs {\n sessionId?: string\n history: Array<{ role: string; content: string }>\n /** Who the assistant is on this surface. The host owns this prose; the server appends tool rules. */\n systemPrompt: string\n truncateAfterMessageId?: string\n hostContext?: any\n toolCategories?: string[]\n requireGroundingTools?: boolean\n}\n\n/**\n * Build the neutral input.\n *\n * Refuses an empty system prompt instead of sending one. A conversation with no persona still\n * answers — it just answers as nobody, on a surface that never said what it is — and that reads\n * as a bad model rather than a missing argument.\n */\nexport function buildAssistantChatInput(args: AssistantChatInputArgs): Record<string, any> {\n if (!args.systemPrompt?.trim()) {\n throw new Error('[ox-board-ai-chat] systemPrompt is required when the chat talks to assistantChat')\n }\n\n return {\n sessionId: args.sessionId ?? null,\n messages: args.history,\n systemPrompt: args.systemPrompt,\n ...(args.truncateAfterMessageId ? { truncateAfterMessageId: args.truncateAfterMessageId } : {}),\n ...(args.hostContext ? { hostContext: args.hostContext } : {}),\n /* Absent means every registered category; an empty array means none. Both are meant, so pass it through. */\n ...(args.toolCategories ? { toolCategories: args.toolCategories } : {}),\n ...(args.requireGroundingTools ? { requireGroundingTools: true } : {})\n }\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,67 @@
|
|
|
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
|
+
const history = [{ role: 'user', content: 'what figures do we have' }];
|
|
15
|
+
const args = (over = {}) => ({ history, systemPrompt: 'you help someone draw a figure', ...over });
|
|
16
|
+
describe('the neutral door carries no document', () => {
|
|
17
|
+
it('★ no board field reaches assistantChat', () => {
|
|
18
|
+
const input = buildAssistantChatInput(args({ sessionId: 's-1' }));
|
|
19
|
+
for (const field of ['currentBoard', 'selectedRefids', 'boardTools', 'knownTypes', 'componentSchemas', 'scopes', 'mentions']) {
|
|
20
|
+
expect(input).not.toHaveProperty(field);
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
it('carries the conversation, the session and the persona', () => {
|
|
24
|
+
const input = buildAssistantChatInput(args({ sessionId: 's-1' }));
|
|
25
|
+
expect(input.sessionId).toBe('s-1');
|
|
26
|
+
expect(input.messages).toBe(history);
|
|
27
|
+
expect(input.systemPrompt).toBe('you help someone draw a figure');
|
|
28
|
+
});
|
|
29
|
+
it('a one-off question has no session — null, not absent, so the server reads it as ad-hoc', () => {
|
|
30
|
+
expect(buildAssistantChatInput(args()).sessionId).toBeNull();
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
describe('the system prompt is required', () => {
|
|
34
|
+
it('★ refuses an empty prompt instead of sending one', () => {
|
|
35
|
+
expect(() => buildAssistantChatInput(args({ systemPrompt: '' }))).toThrow(/systemPrompt is required/);
|
|
36
|
+
expect(() => buildAssistantChatInput(args({ systemPrompt: ' ' }))).toThrow(/systemPrompt is required/);
|
|
37
|
+
expect(() => buildAssistantChatInput(args({ systemPrompt: undefined }))).toThrow(/systemPrompt is required/);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
describe('optional fields are sent only when they mean something', () => {
|
|
41
|
+
it('an unset tool category list is left out — the server default is every registered category', () => {
|
|
42
|
+
expect(buildAssistantChatInput(args())).not.toHaveProperty('toolCategories');
|
|
43
|
+
});
|
|
44
|
+
it('★ an empty tool category list is sent — it means none, which is a different answer', () => {
|
|
45
|
+
expect(buildAssistantChatInput(args({ toolCategories: [] })).toolCategories).toEqual([]);
|
|
46
|
+
});
|
|
47
|
+
it('grounding is sent only when required — false is the server default', () => {
|
|
48
|
+
expect(buildAssistantChatInput(args())).not.toHaveProperty('requireGroundingTools');
|
|
49
|
+
expect(buildAssistantChatInput(args({ requireGroundingTools: true })).requireGroundingTools).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
it('host context and truncation travel when given', () => {
|
|
52
|
+
const input = buildAssistantChatInput(args({ hostContext: { figureId: 'fig-1' }, truncateAfterMessageId: 'm-9' }));
|
|
53
|
+
expect(input.hostContext).toEqual({ figureId: 'fig-1' });
|
|
54
|
+
expect(input.truncateAfterMessageId).toBe('m-9');
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
describe('the board door is unchanged', () => {
|
|
58
|
+
it('★ still carries selectedRefids — a missing one was why the AI did not know what was selected', () => {
|
|
59
|
+
const input = buildChatMutationInput({ history, liveBoard: null, selectedRefids: [3, 7] });
|
|
60
|
+
expect(input.selectedRefids).toEqual([3, 7]);
|
|
61
|
+
});
|
|
62
|
+
it('does not take a system prompt — the board conversation has its persona on the server', () => {
|
|
63
|
+
const input = buildChatMutationInput({ history, liveBoard: null, selectedRefids: [] });
|
|
64
|
+
expect(input).not.toHaveProperty('systemPrompt');
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
//# sourceMappingURL=chat-input-builder.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chat-input-builder.test.js","sourceRoot":"","sources":["../../client/components/chat-input-builder.test.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * What this guards: **the two doors take different inputs, and neither one leaks into the other.**\n *\n * The chat component talks to one of two server operations. `boardAIChat` is board authoring and\n * carries the board model; `assistantChat` is for a surface with no document of its own. Sending\n * a board field to the neutral door, or dropping a required one, produces a GraphQL error the\n * user sees as \"the assistant is broken\".\n *\n * The required system prompt is the one that would be silent. A conversation with no persona\n * still answers — as nobody, on a surface that never said what it is — and that reads as a poor\n * model rather than a missing argument.\n */\nimport { buildAssistantChatInput, buildChatMutationInput } from './chat-input-builder'\n\nconst history = [{ role: 'user', content: 'what figures do we have' }]\n\nconst args = (over: any = {}) => ({ history, systemPrompt: 'you help someone draw a figure', ...over })\n\ndescribe('the neutral door carries no document', () => {\n it('★ no board field reaches assistantChat', () => {\n const input = buildAssistantChatInput(args({ sessionId: 's-1' }))\n\n for (const field of ['currentBoard', 'selectedRefids', 'boardTools', 'knownTypes', 'componentSchemas', 'scopes', 'mentions']) {\n expect(input).not.toHaveProperty(field)\n }\n })\n\n it('carries the conversation, the session and the persona', () => {\n const input = buildAssistantChatInput(args({ sessionId: 's-1' }))\n\n expect(input.sessionId).toBe('s-1')\n expect(input.messages).toBe(history)\n expect(input.systemPrompt).toBe('you help someone draw a figure')\n })\n\n it('a one-off question has no session — null, not absent, so the server reads it as ad-hoc', () => {\n expect(buildAssistantChatInput(args()).sessionId).toBeNull()\n })\n})\n\ndescribe('the system prompt is required', () => {\n it('★ refuses an empty prompt instead of sending one', () => {\n expect(() => buildAssistantChatInput(args({ systemPrompt: '' }))).toThrow(/systemPrompt is required/)\n expect(() => buildAssistantChatInput(args({ systemPrompt: ' ' }))).toThrow(/systemPrompt is required/)\n expect(() => buildAssistantChatInput(args({ systemPrompt: undefined }))).toThrow(/systemPrompt is required/)\n })\n})\n\ndescribe('optional fields are sent only when they mean something', () => {\n it('an unset tool category list is left out — the server default is every registered category', () => {\n expect(buildAssistantChatInput(args())).not.toHaveProperty('toolCategories')\n })\n\n it('★ an empty tool category list is sent — it means none, which is a different answer', () => {\n expect(buildAssistantChatInput(args({ toolCategories: [] })).toolCategories).toEqual([])\n })\n\n it('grounding is sent only when required — false is the server default', () => {\n expect(buildAssistantChatInput(args())).not.toHaveProperty('requireGroundingTools')\n expect(buildAssistantChatInput(args({ requireGroundingTools: true })).requireGroundingTools).toBe(true)\n })\n\n it('host context and truncation travel when given', () => {\n const input = buildAssistantChatInput(args({ hostContext: { figureId: 'fig-1' }, truncateAfterMessageId: 'm-9' }))\n\n expect(input.hostContext).toEqual({ figureId: 'fig-1' })\n expect(input.truncateAfterMessageId).toBe('m-9')\n })\n})\n\ndescribe('the board door is unchanged', () => {\n it('★ still carries selectedRefids — a missing one was why the AI did not know what was selected', () => {\n const input = buildChatMutationInput({ history, liveBoard: null, selectedRefids: [3, 7] } as any)\n expect(input.selectedRefids).toEqual([3, 7])\n })\n\n it('does not take a system prompt — the board conversation has its persona on the server', () => {\n const input = buildChatMutationInput({ history, liveBoard: null, selectedRefids: [] } as any)\n expect(input).not.toHaveProperty('systemPrompt')\n })\n})\n"]}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** What the surface has to offer, as plain facts. The caller reads these off itself. */
|
|
2
|
+
export interface TriggerAvailability {
|
|
3
|
+
/** Something to mention: a live document, a static one, or a declared type list. */
|
|
4
|
+
hasMentionableThings: boolean;
|
|
5
|
+
/** A stored conversation. A `@` mention notifies a participant, and an ad-hoc turn has none. */
|
|
6
|
+
hasSession: boolean;
|
|
7
|
+
/** How many insertable templates the host registered. */
|
|
8
|
+
slashTemplateCount: number;
|
|
9
|
+
}
|
|
10
|
+
export interface TriggerChip {
|
|
11
|
+
char: '#' | '@' | '/';
|
|
12
|
+
labelKey: string;
|
|
13
|
+
tooltipKey: string;
|
|
14
|
+
}
|
|
15
|
+
/** Order is fixed: `#` `@` `/`. It is the order people learned, so it does not follow availability. */
|
|
16
|
+
export declare function availableTriggers(availability: TriggerAvailability): TriggerChip[];
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Which trigger characters a chat surface can actually use — **asked, not declared.**
|
|
3
|
+
*
|
|
4
|
+
* All three chips (`#` `@` `/`) were always drawn. On operato-figure that taught two things the
|
|
5
|
+
* surface cannot do: `#` mentions components of a document figure has none of, and `/` inserts
|
|
6
|
+
* templates figure registered none of, so the popup opens empty.
|
|
7
|
+
*
|
|
8
|
+
* A chip that opens nothing is worse than an absent one. The reader does not conclude "not
|
|
9
|
+
* applicable here" — they conclude the feature is broken, and they are half right.
|
|
10
|
+
*
|
|
11
|
+
* So each chip is read off the thing it needs. A host that gains a document, a stored session or
|
|
12
|
+
* a template set gets the chip with no extra declaration; one that never does never shows it.
|
|
13
|
+
* Same shape as lite-menu deciding whether the board menu type is selectable by asking whether
|
|
14
|
+
* its renderer is registered, rather than declaring that it is.
|
|
15
|
+
*/
|
|
16
|
+
/** Order is fixed: `#` `@` `/`. It is the order people learned, so it does not follow availability. */
|
|
17
|
+
export function availableTriggers(availability) {
|
|
18
|
+
const chips = [];
|
|
19
|
+
if (availability.hasMentionableThings) {
|
|
20
|
+
chips.push({
|
|
21
|
+
char: '#',
|
|
22
|
+
labelKey: 'ai-assistant.label.trigger-component',
|
|
23
|
+
tooltipKey: 'ai-assistant.text.trigger-component-tooltip'
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
if (availability.hasSession) {
|
|
27
|
+
chips.push({
|
|
28
|
+
char: '@',
|
|
29
|
+
labelKey: 'ai-assistant.label.trigger-user',
|
|
30
|
+
tooltipKey: 'ai-assistant.text.trigger-user-tooltip'
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
if (availability.slashTemplateCount > 0) {
|
|
34
|
+
chips.push({
|
|
35
|
+
char: '/',
|
|
36
|
+
labelKey: 'ai-assistant.label.trigger-insert',
|
|
37
|
+
tooltipKey: 'ai-assistant.text.trigger-insert-tooltip'
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
return chips;
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=chat-triggers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chat-triggers.js","sourceRoot":"","sources":["../../client/components/chat-triggers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAkBH,uGAAuG;AACvG,MAAM,UAAU,iBAAiB,CAAC,YAAiC;IACjE,MAAM,KAAK,GAAkB,EAAE,CAAA;IAE/B,IAAI,YAAY,CAAC,oBAAoB,EAAE,CAAC;QACtC,KAAK,CAAC,IAAI,CAAC;YACT,IAAI,EAAE,GAAG;YACT,QAAQ,EAAE,sCAAsC;YAChD,UAAU,EAAE,6CAA6C;SAC1D,CAAC,CAAA;IACJ,CAAC;IAED,IAAI,YAAY,CAAC,UAAU,EAAE,CAAC;QAC5B,KAAK,CAAC,IAAI,CAAC;YACT,IAAI,EAAE,GAAG;YACT,QAAQ,EAAE,iCAAiC;YAC3C,UAAU,EAAE,wCAAwC;SACrD,CAAC,CAAA;IACJ,CAAC;IAED,IAAI,YAAY,CAAC,kBAAkB,GAAG,CAAC,EAAE,CAAC;QACxC,KAAK,CAAC,IAAI,CAAC;YACT,IAAI,EAAE,GAAG;YACT,QAAQ,EAAE,mCAAmC;YAC7C,UAAU,EAAE,0CAA0C;SACvD,CAAC,CAAA;IACJ,CAAC;IAED,OAAO,KAAK,CAAA;AACd,CAAC","sourcesContent":["/*\n * Which trigger characters a chat surface can actually use — **asked, not declared.**\n *\n * All three chips (`#` `@` `/`) were always drawn. On operato-figure that taught two things the\n * surface cannot do: `#` mentions components of a document figure has none of, and `/` inserts\n * templates figure registered none of, so the popup opens empty.\n *\n * A chip that opens nothing is worse than an absent one. The reader does not conclude \"not\n * applicable here\" — they conclude the feature is broken, and they are half right.\n *\n * So each chip is read off the thing it needs. A host that gains a document, a stored session or\n * a template set gets the chip with no extra declaration; one that never does never shows it.\n * Same shape as lite-menu deciding whether the board menu type is selectable by asking whether\n * its renderer is registered, rather than declaring that it is.\n */\n\n/** What the surface has to offer, as plain facts. The caller reads these off itself. */\nexport interface TriggerAvailability {\n /** Something to mention: a live document, a static one, or a declared type list. */\n hasMentionableThings: boolean\n /** A stored conversation. A `@` mention notifies a participant, and an ad-hoc turn has none. */\n hasSession: boolean\n /** How many insertable templates the host registered. */\n slashTemplateCount: number\n}\n\nexport interface TriggerChip {\n char: '#' | '@' | '/'\n labelKey: string\n tooltipKey: string\n}\n\n/** Order is fixed: `#` `@` `/`. It is the order people learned, so it does not follow availability. */\nexport function availableTriggers(availability: TriggerAvailability): TriggerChip[] {\n const chips: TriggerChip[] = []\n\n if (availability.hasMentionableThings) {\n chips.push({\n char: '#',\n labelKey: 'ai-assistant.label.trigger-component',\n tooltipKey: 'ai-assistant.text.trigger-component-tooltip'\n })\n }\n\n if (availability.hasSession) {\n chips.push({\n char: '@',\n labelKey: 'ai-assistant.label.trigger-user',\n tooltipKey: 'ai-assistant.text.trigger-user-tooltip'\n })\n }\n\n if (availability.slashTemplateCount > 0) {\n chips.push({\n char: '/',\n labelKey: 'ai-assistant.label.trigger-insert',\n tooltipKey: 'ai-assistant.text.trigger-insert-tooltip'\n })\n }\n\n return chips\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* What this guards: **the empty chat does not teach what this surface cannot do.**
|
|
3
|
+
*
|
|
4
|
+
* operato-figure mounted the chat and its empty screen offered all three triggers. `#` mentions
|
|
5
|
+
* components of a document figure has none of; `/` inserts templates figure registered none of,
|
|
6
|
+
* so the popup opens empty. Neither errors — they just do nothing, which reads as broken rather
|
|
7
|
+
* than not applicable.
|
|
8
|
+
*/
|
|
9
|
+
import { availableTriggers } from './chat-triggers';
|
|
10
|
+
const chars = (availability) => availableTriggers(availability).map(chip => chip.char);
|
|
11
|
+
const board = { hasMentionableThings: true, hasSession: true, slashTemplateCount: 12 };
|
|
12
|
+
const figure = { hasMentionableThings: false, hasSession: true, slashTemplateCount: 0 };
|
|
13
|
+
describe('a board authoring surface', () => {
|
|
14
|
+
it('offers all three', () => {
|
|
15
|
+
expect(chars(board)).toEqual(['#', '@', '/']);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
describe('a surface with no document of its own', () => {
|
|
19
|
+
it('★ does not offer # — there is nothing to mention', () => {
|
|
20
|
+
expect(chars(figure)).not.toContain('#');
|
|
21
|
+
});
|
|
22
|
+
it('★ does not offer / — nobody registered a template, so the popup would open empty', () => {
|
|
23
|
+
expect(chars(figure)).not.toContain('/');
|
|
24
|
+
});
|
|
25
|
+
it('still offers @ — mentioning a colleague works wherever a conversation is stored', () => {
|
|
26
|
+
expect(chars(figure)).toEqual(['@']);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
describe('each chip is read off the one thing it needs', () => {
|
|
30
|
+
it('a declared type list is enough for # — a live document is not required', () => {
|
|
31
|
+
expect(chars({ ...figure, hasMentionableThings: true })).toContain('#');
|
|
32
|
+
});
|
|
33
|
+
it('an ad-hoc turn has no @ — a mention notifies a participant and there is no conversation to join', () => {
|
|
34
|
+
expect(chars({ ...board, hasSession: false })).not.toContain('@');
|
|
35
|
+
});
|
|
36
|
+
it('one registered template is enough for /', () => {
|
|
37
|
+
expect(chars({ ...figure, slashTemplateCount: 1 })).toContain('/');
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
describe('what the row looks like when nothing is available', () => {
|
|
41
|
+
it('★ empty, so the caller can leave the whole row out rather than draw a heading over nothing', () => {
|
|
42
|
+
expect(availableTriggers({ hasMentionableThings: false, hasSession: false, slashTemplateCount: 0 })).toEqual([]);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
describe('order', () => {
|
|
46
|
+
it('stays # @ / regardless of which ones survive — it is the order people learned', () => {
|
|
47
|
+
expect(chars({ hasMentionableThings: true, hasSession: false, slashTemplateCount: 3 })).toEqual(['#', '/']);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
describe('every chip carries its own words', () => {
|
|
51
|
+
it('label and tooltip keys are distinct per chip — one shared key would caption them all alike', () => {
|
|
52
|
+
const chips = availableTriggers(board);
|
|
53
|
+
const labels = new Set(chips.map(chip => chip.labelKey));
|
|
54
|
+
const tooltips = new Set(chips.map(chip => chip.tooltipKey));
|
|
55
|
+
expect(labels.size).toBe(chips.length);
|
|
56
|
+
expect(tooltips.size).toBe(chips.length);
|
|
57
|
+
for (const chip of chips)
|
|
58
|
+
expect(chip.labelKey.startsWith('ai-assistant.')).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
//# sourceMappingURL=chat-triggers.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chat-triggers.test.js","sourceRoot":"","sources":["../../client/components/chat-triggers.test.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * What this guards: **the empty chat does not teach what this surface cannot do.**\n *\n * operato-figure mounted the chat and its empty screen offered all three triggers. `#` mentions\n * components of a document figure has none of; `/` inserts templates figure registered none of,\n * so the popup opens empty. Neither errors — they just do nothing, which reads as broken rather\n * than not applicable.\n */\nimport { availableTriggers } from './chat-triggers'\n\nconst chars = (availability: any) => availableTriggers(availability).map(chip => chip.char)\n\nconst board = { hasMentionableThings: true, hasSession: true, slashTemplateCount: 12 }\nconst figure = { hasMentionableThings: false, hasSession: true, slashTemplateCount: 0 }\n\ndescribe('a board authoring surface', () => {\n it('offers all three', () => {\n expect(chars(board)).toEqual(['#', '@', '/'])\n })\n})\n\ndescribe('a surface with no document of its own', () => {\n it('★ does not offer # — there is nothing to mention', () => {\n expect(chars(figure)).not.toContain('#')\n })\n\n it('★ does not offer / — nobody registered a template, so the popup would open empty', () => {\n expect(chars(figure)).not.toContain('/')\n })\n\n it('still offers @ — mentioning a colleague works wherever a conversation is stored', () => {\n expect(chars(figure)).toEqual(['@'])\n })\n})\n\ndescribe('each chip is read off the one thing it needs', () => {\n it('a declared type list is enough for # — a live document is not required', () => {\n expect(chars({ ...figure, hasMentionableThings: true })).toContain('#')\n })\n\n it('an ad-hoc turn has no @ — a mention notifies a participant and there is no conversation to join', () => {\n expect(chars({ ...board, hasSession: false })).not.toContain('@')\n })\n\n it('one registered template is enough for /', () => {\n expect(chars({ ...figure, slashTemplateCount: 1 })).toContain('/')\n })\n})\n\ndescribe('what the row looks like when nothing is available', () => {\n it('★ empty, so the caller can leave the whole row out rather than draw a heading over nothing', () => {\n expect(availableTriggers({ hasMentionableThings: false, hasSession: false, slashTemplateCount: 0 })).toEqual([])\n })\n})\n\ndescribe('order', () => {\n it('stays # @ / regardless of which ones survive — it is the order people learned', () => {\n expect(chars({ hasMentionableThings: true, hasSession: false, slashTemplateCount: 3 })).toEqual(['#', '/'])\n })\n})\n\ndescribe('every chip carries its own words', () => {\n it('label and tooltip keys are distinct per chip — one shared key would caption them all alike', () => {\n const chips = availableTriggers(board)\n const labels = new Set(chips.map(chip => chip.labelKey))\n const tooltips = new Set(chips.map(chip => chip.tooltipKey))\n\n expect(labels.size).toBe(chips.length)\n expect(tooltips.size).toBe(chips.length)\n for (const chip of chips) expect(chip.labelKey.startsWith('ai-assistant.')).toBe(true)\n })\n})\n"]}
|