@super_studio/ecforce-ai-agent-react 1.4.4 → 1.5.0
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/README.md +34 -0
- package/dist/components/chatbot-sheet.d.mts.map +1 -1
- package/dist/components/provider/chatbot-provider.d.mts +2 -0
- package/dist/components/provider/chatbot-provider.d.mts.map +1 -1
- package/dist/components/provider/chatbot-provider.mjs +2 -1
- package/dist/components/provider/chatbot-provider.mjs.map +1 -1
- package/dist/components/provider/use-chatbot-frame-handler.mjs +30 -1
- package/dist/components/provider/use-chatbot-frame-handler.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/provider/chatbot-provider.tsx +4 -0
- package/src/components/provider/use-chatbot-frame-handler.tsx +47 -1
package/README.md
CHANGED
|
@@ -222,6 +222,40 @@ sendAppMessage({
|
|
|
222
222
|
|
|
223
223
|
これにより、AI Agent 側で現在の画面や対象データに応じた応答をしやすくなります。
|
|
224
224
|
|
|
225
|
+
#### 送信せず、入力欄に入れるだけにしたい場合は `setInput()`
|
|
226
|
+
|
|
227
|
+
`sendAppMessage()` はそのまま送信されます。**ユーザーに内容を確認・編集してもらってから送信させたい**場合は、`useChatbot()` の `setInput()` を使います。
|
|
228
|
+
|
|
229
|
+
```tsx
|
|
230
|
+
"use client";
|
|
231
|
+
|
|
232
|
+
import { useChatbot } from "@super_studio/ecforce-ai-agent-react";
|
|
233
|
+
|
|
234
|
+
export function SalesReportButton() {
|
|
235
|
+
const { setInput } = useChatbot();
|
|
236
|
+
|
|
237
|
+
return (
|
|
238
|
+
<button
|
|
239
|
+
type="button"
|
|
240
|
+
onClick={() =>
|
|
241
|
+
setInput("直近1週間の売上分析を行い、キャンバス上のレポートにまとめてください")
|
|
242
|
+
}
|
|
243
|
+
>
|
|
244
|
+
売上レポートを作る
|
|
245
|
+
</button>
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
`sendAppMessage()` と同じく、**チャットが閉じていても自動で開き**、iframe の初期化が終わっていない場合はテキストを一時保持して準備完了後に反映します。送信はユーザーが送信ボタンを押すまで行われません。
|
|
251
|
+
|
|
252
|
+
| | `sendAppMessage()` | `setInput()` |
|
|
253
|
+
|---|---|---|
|
|
254
|
+
| 送信 | する | **しない**(入力欄に入れるだけ) |
|
|
255
|
+
| チャットを自動で開く | する | する |
|
|
256
|
+
| 初期化前の呼び出し | 保持して後で送る | 保持して後で反映する |
|
|
257
|
+
| hidden な文脈・添付 | 渡せる | 渡せない(テキストのみ) |
|
|
258
|
+
|
|
225
259
|
### 3-5. `useClientTool()` でクライアントツールを登録する
|
|
226
260
|
|
|
227
261
|
ブラウザ上で実行したい処理を AI Agent から呼ばせたい場合は、`useClientTool()` を使います。
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chatbot-sheet.d.mts","names":[],"sources":["../../src/components/chatbot-sheet.tsx"],"sourcesContent":[],"mappings":";;;;;KAcK,iBAAA,GAAoB;eACV,KAAA,CAAM;;AADhB,iBAIW,YAAA,CAJS;EAAA,UAAA;EAAA,
|
|
1
|
+
{"version":3,"file":"chatbot-sheet.d.mts","names":[],"sources":["../../src/components/chatbot-sheet.tsx"],"sourcesContent":[],"mappings":";;;;;KAcK,iBAAA,GAAoB;eACV,KAAA,CAAM;;AADhB,iBAIW,YAAA,CAJS;EAAA,UAAA;EAAA,GACV;AAAmB,CAAb,EAGkC,iBAHrB,CAAA,EAGsC,kBAAA,CAAA,GAAA,CAAA,OAHtC"}
|
|
@@ -27,6 +27,8 @@ type ChatbotContextType = {
|
|
|
27
27
|
setAppName: (appName: string) => void;
|
|
28
28
|
/** セッショントークンを設定 */
|
|
29
29
|
setSessionToken: (sessionToken: string) => void;
|
|
30
|
+
/** 入力欄にテキストを流し込む(送信はしない) */
|
|
31
|
+
setInput: (text: string) => void;
|
|
30
32
|
/** アプリからメッセージを送信 */
|
|
31
33
|
sendAppMessage: (payload: SendAppMessagePayload) => void;
|
|
32
34
|
/** Client Toolを登録 */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chatbot-provider.d.mts","names":[],"sources":["../../../src/components/provider/chatbot-provider.tsx"],"sourcesContent":[],"mappings":";;;;;;KAYK,kBAAA;;;EAAA;EAgBW,IAAA,EAAA,OAAA;EAEE;
|
|
1
|
+
{"version":3,"file":"chatbot-provider.d.mts","names":[],"sources":["../../../src/components/provider/chatbot-provider.tsx"],"sourcesContent":[],"mappings":";;;;;;KAYK,kBAAA;;;EAAA;EAgBW,IAAA,EAAA,OAAA;EAEE;EAQU,OAAA,EAAA,CAAA,IAAA,EAAA,OAAA,EAAA,GAAA,IAAA;EAEC;EAWH,UAAA,EAAA,OAAA;EAAiB;EAmC3B,aAAU,EAAA,CAAA,QAAA,EAAA,OAAA,EAAA,GAAA,IAAA;EAQrB;EAIW,YAAA,EAAA,OAAe;EAAG;EAAY,eAAA,EAAA,CAAA,UAAA,EAAA,OAAA,EAAA,GAAA,IAAA;EAAoB;EAAA,IAAA,EAAA,CAAA,KAAA,EAtElD,SAsEkD,EAAA,GAAA,IAAA;;kBApEhD;;;;;;;;4BAQU;;6BAEC;;;;;;;;;;;0BAWH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAmCV,UAAA,CAAA,GAAU;KAQrB,oBAAA;YACO,KAAA,CAAM;;iBAGF,eAAA;;GAA8B,uBAAoB,kBAAA,CAAA,GAAA,CAAA"}
|
|
@@ -42,7 +42,7 @@ function useChatbot() {
|
|
|
42
42
|
}
|
|
43
43
|
function ChatbotProvider({ children }) {
|
|
44
44
|
const { hasOpened, open, isExpanded, isFullScreen, setOpen, setIsExpanded, setIsFullScreen } = useChatbotWindowStates();
|
|
45
|
-
const { setMcps, setAppName, setSessionToken, sendAppMessage, registerClientTool, unregisterClientTool, init, isReady, isInitialized, setIframeEl } = useChatbotFrameHandler({
|
|
45
|
+
const { setMcps, setAppName, setSessionToken, setInput, sendAppMessage, registerClientTool, unregisterClientTool, init, isReady, isInitialized, setIframeEl } = useChatbotFrameHandler({
|
|
46
46
|
setOpen,
|
|
47
47
|
setIsExpanded,
|
|
48
48
|
setIsFullScreen
|
|
@@ -56,6 +56,7 @@ function ChatbotProvider({ children }) {
|
|
|
56
56
|
isFullScreen,
|
|
57
57
|
setIsFullScreen,
|
|
58
58
|
setSessionToken,
|
|
59
|
+
setInput,
|
|
59
60
|
sendAppMessage,
|
|
60
61
|
registerClientTool,
|
|
61
62
|
unregisterClientTool,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chatbot-provider.mjs","names":[],"sources":["../../../src/components/provider/chatbot-provider.tsx"],"sourcesContent":["\"use client\";\n\nimport React from \"react\";\nimport type { RegisteredClientTool } from \"../../lib/client-tools\";\nimport {\n type InitProps,\n type MCP,\n type SendAppMessagePayload,\n useChatbotFrameHandler,\n} from \"./use-chatbot-frame-handler\";\nimport { useChatbotWindowStates } from \"./use-chatbot-window-states\";\n\ntype ChatbotContextType = {\n /** 1回チャットボットを開いたら、固定でtrueになる */\n hasOpened: boolean;\n /** チャットボットを開いているか */\n open: boolean;\n /** チャットボットを開く */\n setOpen: (open: boolean) => void;\n /** チャットボットを展開しているか */\n isExpanded: boolean;\n /** チャットボットを展開する */\n setIsExpanded: (expanded: boolean) => void;\n /** チャットボットをフルスクリーンにしているか */\n isFullScreen: boolean;\n /** チャットボットをフルスクリーンにする */\n setIsFullScreen: (fullScreen: boolean) => void;\n /** チャットボットを初期化 */\n init: (props: InitProps) => void;\n /** MCPを設定 */\n setMcps: (mcps: MCP[]) => void;\n /** アプリ名を設定 */\n setAppName: (appName: string) => void;\n /** セッショントークンを設定 */\n setSessionToken: (sessionToken: string) => void;\n /** アプリからメッセージを送信 */\n sendAppMessage: (payload: SendAppMessagePayload) => void;\n /** Client Toolを登録 */\n registerClientTool: (tool: RegisteredClientTool) => void;\n /** Client Toolを解除 */\n unregisterClientTool: (name: string) => void;\n /** チャットボットが準備できたか */\n isReady: boolean;\n /** チャットボットが初期化されたか */\n isInitialized: boolean;\n /**\n * @internal\n * チャットボットのiframe要素を設定\n */\n setIframeEl: (iframeEl: HTMLIFrameElement | null) => void;\n};\n\nconst ChatbotContext = React.createContext<ChatbotContextType | undefined>(\n undefined,\n);\n\n/**\n * `ChatbotProvider` が提供するチャットボット状態と操作 API を取得します。\n *\n * @returns チャットボットの表示状態、初期化 API、Client Tool の登録 API を含むコンテキスト値。\n *\n * @example\n * ```tsx\n * import { useChatbot } from \"@super_studio/ecforce-ai-agent-react\";\n *\n * export function OpenChatButton() {\n * const { open, setOpen, sendAppMessage } = useChatbot();\n *\n * return (\n * <button\n * type=\"button\"\n * onClick={() => {\n * setOpen(!open);\n * sendAppMessage({\n * message: \"EC Forceの注文状況を確認したいです\",\n * });\n * }}\n * >\n * Open chat\n * </button>\n * );\n * }\n * ```\n */\nexport function useChatbot() {\n const context = React.useContext(ChatbotContext);\n if (!context) {\n throw new Error(\"useChatbot must be used within a ChatbotProvider\");\n }\n return context;\n}\n\ntype ChatbotProviderProps = {\n children: React.ReactNode;\n};\n\nexport function ChatbotProvider({ children }: ChatbotProviderProps) {\n const {\n hasOpened,\n open,\n isExpanded,\n isFullScreen,\n setOpen,\n setIsExpanded,\n setIsFullScreen,\n } = useChatbotWindowStates();\n const {\n setMcps,\n setAppName,\n setSessionToken,\n sendAppMessage,\n registerClientTool,\n unregisterClientTool,\n init,\n isReady,\n isInitialized,\n setIframeEl,\n } = useChatbotFrameHandler({\n setOpen,\n setIsExpanded,\n setIsFullScreen,\n });\n\n const value = {\n hasOpened,\n open,\n setOpen,\n isExpanded,\n setIsExpanded,\n isFullScreen,\n setIsFullScreen,\n setSessionToken,\n sendAppMessage,\n registerClientTool,\n unregisterClientTool,\n init,\n setMcps,\n setAppName,\n isReady,\n isInitialized,\n setIframeEl,\n };\n\n return (\n <ChatbotContext.Provider value={value}>{children}</ChatbotContext.Provider>\n );\n}\n"],"mappings":";;;;;;;;
|
|
1
|
+
{"version":3,"file":"chatbot-provider.mjs","names":[],"sources":["../../../src/components/provider/chatbot-provider.tsx"],"sourcesContent":["\"use client\";\n\nimport React from \"react\";\nimport type { RegisteredClientTool } from \"../../lib/client-tools\";\nimport {\n type InitProps,\n type MCP,\n type SendAppMessagePayload,\n useChatbotFrameHandler,\n} from \"./use-chatbot-frame-handler\";\nimport { useChatbotWindowStates } from \"./use-chatbot-window-states\";\n\ntype ChatbotContextType = {\n /** 1回チャットボットを開いたら、固定でtrueになる */\n hasOpened: boolean;\n /** チャットボットを開いているか */\n open: boolean;\n /** チャットボットを開く */\n setOpen: (open: boolean) => void;\n /** チャットボットを展開しているか */\n isExpanded: boolean;\n /** チャットボットを展開する */\n setIsExpanded: (expanded: boolean) => void;\n /** チャットボットをフルスクリーンにしているか */\n isFullScreen: boolean;\n /** チャットボットをフルスクリーンにする */\n setIsFullScreen: (fullScreen: boolean) => void;\n /** チャットボットを初期化 */\n init: (props: InitProps) => void;\n /** MCPを設定 */\n setMcps: (mcps: MCP[]) => void;\n /** アプリ名を設定 */\n setAppName: (appName: string) => void;\n /** セッショントークンを設定 */\n setSessionToken: (sessionToken: string) => void;\n /** 入力欄にテキストを流し込む(送信はしない) */\n setInput: (text: string) => void;\n /** アプリからメッセージを送信 */\n sendAppMessage: (payload: SendAppMessagePayload) => void;\n /** Client Toolを登録 */\n registerClientTool: (tool: RegisteredClientTool) => void;\n /** Client Toolを解除 */\n unregisterClientTool: (name: string) => void;\n /** チャットボットが準備できたか */\n isReady: boolean;\n /** チャットボットが初期化されたか */\n isInitialized: boolean;\n /**\n * @internal\n * チャットボットのiframe要素を設定\n */\n setIframeEl: (iframeEl: HTMLIFrameElement | null) => void;\n};\n\nconst ChatbotContext = React.createContext<ChatbotContextType | undefined>(\n undefined,\n);\n\n/**\n * `ChatbotProvider` が提供するチャットボット状態と操作 API を取得します。\n *\n * @returns チャットボットの表示状態、初期化 API、Client Tool の登録 API を含むコンテキスト値。\n *\n * @example\n * ```tsx\n * import { useChatbot } from \"@super_studio/ecforce-ai-agent-react\";\n *\n * export function OpenChatButton() {\n * const { open, setOpen, sendAppMessage } = useChatbot();\n *\n * return (\n * <button\n * type=\"button\"\n * onClick={() => {\n * setOpen(!open);\n * sendAppMessage({\n * message: \"EC Forceの注文状況を確認したいです\",\n * });\n * }}\n * >\n * Open chat\n * </button>\n * );\n * }\n * ```\n */\nexport function useChatbot() {\n const context = React.useContext(ChatbotContext);\n if (!context) {\n throw new Error(\"useChatbot must be used within a ChatbotProvider\");\n }\n return context;\n}\n\ntype ChatbotProviderProps = {\n children: React.ReactNode;\n};\n\nexport function ChatbotProvider({ children }: ChatbotProviderProps) {\n const {\n hasOpened,\n open,\n isExpanded,\n isFullScreen,\n setOpen,\n setIsExpanded,\n setIsFullScreen,\n } = useChatbotWindowStates();\n const {\n setMcps,\n setAppName,\n setSessionToken,\n setInput,\n sendAppMessage,\n registerClientTool,\n unregisterClientTool,\n init,\n isReady,\n isInitialized,\n setIframeEl,\n } = useChatbotFrameHandler({\n setOpen,\n setIsExpanded,\n setIsFullScreen,\n });\n\n const value = {\n hasOpened,\n open,\n setOpen,\n isExpanded,\n setIsExpanded,\n isFullScreen,\n setIsFullScreen,\n setSessionToken,\n setInput,\n sendAppMessage,\n registerClientTool,\n unregisterClientTool,\n init,\n setMcps,\n setAppName,\n isReady,\n isInitialized,\n setIframeEl,\n };\n\n return (\n <ChatbotContext.Provider value={value}>{children}</ChatbotContext.Provider>\n );\n}\n"],"mappings":";;;;;;;;AAsDA,MAAM,iBAAiB,MAAM,cAC3B,OACD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BD,SAAgB,aAAa;CAC3B,MAAM,UAAU,MAAM,WAAW,eAAe;AAChD,KAAI,CAAC,QACH,OAAM,IAAI,MAAM,mDAAmD;AAErE,QAAO;;AAOT,SAAgB,gBAAgB,EAAE,YAAkC;CAClE,MAAM,EACJ,WACA,MACA,YACA,cACA,SACA,eACA,oBACE,wBAAwB;CAC5B,MAAM,EACJ,SACA,YACA,iBACA,UACA,gBACA,oBACA,sBACA,MACA,SACA,eACA,gBACE,uBAAuB;EACzB;EACA;EACA;EACD,CAAC;CAEF,MAAM,QAAQ;EACZ;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACD;AAED,QACE,oBAAC,eAAe;EAAgB;EAAQ;GAAmC"}
|
|
@@ -43,6 +43,7 @@ function useChatbotFrameHandler({ setOpen, setIsExpanded, setIsFullScreen }) {
|
|
|
43
43
|
const [isReady, setIsReady] = React.useState(false);
|
|
44
44
|
const [isInitialized, setIsInitialized] = React.useState(false);
|
|
45
45
|
const pendingAppMessageRef = React.useRef(null);
|
|
46
|
+
const pendingInputRef = React.useRef(null);
|
|
46
47
|
const clientToolRegistryRef = React.useRef(/* @__PURE__ */ new Map());
|
|
47
48
|
const postMessage = React.useCallback((message) => {
|
|
48
49
|
if (iframeEl === null || iframeEl === void 0 ? void 0 : iframeEl.contentWindow) {
|
|
@@ -73,6 +74,32 @@ function useChatbotFrameHandler({ setOpen, setIsExpanded, setIsFullScreen }) {
|
|
|
73
74
|
sessionToken
|
|
74
75
|
});
|
|
75
76
|
}, [postMessage]);
|
|
77
|
+
const flushPendingInput = React.useCallback(() => {
|
|
78
|
+
const pendingText = pendingInputRef.current;
|
|
79
|
+
if (pendingText === null || !isReady || !isInitialized) return false;
|
|
80
|
+
const sent = postMessage({
|
|
81
|
+
type: "setInput",
|
|
82
|
+
text: pendingText
|
|
83
|
+
});
|
|
84
|
+
if (sent) pendingInputRef.current = null;
|
|
85
|
+
return sent;
|
|
86
|
+
}, [
|
|
87
|
+
isInitialized,
|
|
88
|
+
isReady,
|
|
89
|
+
postMessage
|
|
90
|
+
]);
|
|
91
|
+
/**
|
|
92
|
+
* 入力欄にテキストを流し込む(送信はしない)。
|
|
93
|
+
*
|
|
94
|
+
* sendAppMessage() と同じく、閉じていればパネルを開き、iframe の準備が
|
|
95
|
+
* 整うまでテキストを保持する。postMessage は iframe 未マウント時に false を
|
|
96
|
+
* 返して破棄されるため、保持しないと「開いた直後に呼ぶと消える」ことになる。
|
|
97
|
+
*/
|
|
98
|
+
const setInput = React.useCallback((text) => {
|
|
99
|
+
pendingInputRef.current = text;
|
|
100
|
+
setOpen(true);
|
|
101
|
+
flushPendingInput();
|
|
102
|
+
}, [flushPendingInput, setOpen]);
|
|
76
103
|
const flushPendingAppMessage = React.useCallback(() => {
|
|
77
104
|
const pendingPayload = pendingAppMessageRef.current;
|
|
78
105
|
if (!pendingPayload || !isReady || !isInitialized) return false;
|
|
@@ -94,8 +121,9 @@ function useChatbotFrameHandler({ setOpen, setIsExpanded, setIsFullScreen }) {
|
|
|
94
121
|
flushPendingAppMessage();
|
|
95
122
|
}, [flushPendingAppMessage, setOpen]);
|
|
96
123
|
React.useEffect(() => {
|
|
124
|
+
flushPendingInput();
|
|
97
125
|
flushPendingAppMessage();
|
|
98
|
-
}, [flushPendingAppMessage]);
|
|
126
|
+
}, [flushPendingInput, flushPendingAppMessage]);
|
|
99
127
|
const registerClientTool = React.useCallback((tool) => {
|
|
100
128
|
clientToolRegistryRef.current.set(tool.definition.name, tool);
|
|
101
129
|
if (!isReady || !isInitialized) return;
|
|
@@ -210,6 +238,7 @@ function useChatbotFrameHandler({ setOpen, setIsExpanded, setIsFullScreen }) {
|
|
|
210
238
|
setMcps,
|
|
211
239
|
setAppName,
|
|
212
240
|
setSessionToken,
|
|
241
|
+
setInput,
|
|
213
242
|
sendAppMessage,
|
|
214
243
|
registerClientTool,
|
|
215
244
|
unregisterClientTool,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-chatbot-frame-handler.mjs","names":[],"sources":["../../../src/components/provider/use-chatbot-frame-handler.tsx"],"sourcesContent":["\"use client\";\n\nimport React from \"react\";\nimport type {\n ClientToolDefinition,\n ClientToolExecuteRequest,\n RegisteredClientTool,\n} from \"../../lib/client-tools\";\n\nexport type IframeMessage =\n | { type: \"CHATBOT_READY\" }\n | { type: \"CHATBOT_INITIALIZED\" }\n | { type: \"CLOSE_CHATBOT\" }\n | { type: \"EXPAND_CHATBOT\" }\n | { type: \"SHRINK_CHATBOT\" }\n | { type: \"FULLSCREEN_CHATBOT\" }\n | { type: \"EXIT_FULLSCREEN_CHATBOT\" }\n | { type: \"RELOAD_CHATBOT\" }\n | ({\n type: \"EXECUTE_CLIENT_TOOL\";\n } & ClientToolExecuteRequest);\n\nexport type MCP = {\n name: string;\n url: string;\n headers?: Record<string, string>;\n};\n\nexport type InitProps = {\n mcps?: MCP[];\n appName?: string;\n sessionToken?: string;\n};\n\nexport type AppMessageAttachment = {\n /** 添付ファイルの表示名です。 */\n fileName: string;\n /** 添付ファイルの MIME type です。 */\n mediaType: string;\n /** 添付ファイルを取得できる URL です。 */\n url: string;\n};\n\nexport type SendAppMessagePayload = {\n /** チャット上に表示するタイトルです。 */\n title?: string;\n /** ユーザーに表示するメッセージ本文です。 */\n message: string;\n /** UI に表示せず、裏側のコンテキストとして渡すメッセージです。 */\n hiddenMessage?: string;\n /** 新規チャットを開始してから送るかを指定します。 */\n startOnNewChat?: boolean;\n /** メッセージと一緒に渡す添付ファイル一覧です。 */\n attachments?: AppMessageAttachment[];\n /** このメッセージ送信で使用するモデルの alias key です。 */\n model?: string;\n};\n\nexport type ParentMessage =\n | {\n type: \"mcps\";\n mcps: MCP[];\n }\n | {\n type: \"appName\";\n appName: string;\n }\n | {\n type: \"sessionToken\";\n sessionToken: string;\n }\n | {\n type: \"init\";\n mcps?: MCP[];\n appName?: string;\n sessionToken?: string;\n }\n | {\n type: \"appMessage\";\n payload: SendAppMessagePayload;\n }\n | {\n type: \"registerClientTools\";\n tools: ClientToolDefinition[];\n }\n | {\n type: \"unregisterClientTools\";\n names: string[];\n }\n | {\n type: \"clientToolResult\";\n callId: string;\n result?: unknown;\n error?: string;\n };\n\nfunction serializeError(error: unknown) {\n if (error instanceof Error) {\n return error.message;\n }\n\n if (typeof error === \"string\") {\n return error;\n }\n\n return \"Unknown client tool error\";\n}\n\ntype UseChatbotFrameHandlerProps = {\n /** チャットボットの開閉状態を更新します。 */\n setOpen: (open: boolean) => void;\n /** 展開状態を更新します。 */\n setIsExpanded: (expanded: boolean) => void;\n /** フルスクリーン状態を更新します。 */\n setIsFullScreen: (fullScreen: boolean) => void;\n};\n\n/**\n * iframe と親アプリ間のメッセージ送受信を管理し、チャットボット制御 API をまとめます。\n *\n * @returns iframe 初期化、メッセージ送信、Client Tool 登録に使うハンドラ一式を返します。\n *\n * @example\n * ```tsx\n * export function FrameBridge() {\n * const [open, setOpen] = React.useState(false);\n * const [isExpanded, setIsExpanded] = React.useState(false);\n * const [isFullScreen, setIsFullScreen] = React.useState(false);\n * const { setIframeEl, init, sendAppMessage, isReady } =\n * useChatbotFrameHandler({\n * setOpen,\n * setIsExpanded,\n * setIsFullScreen,\n * });\n *\n * React.useEffect(() => {\n * if (isReady) {\n * init({ appName: \"EC Force\" });\n * sendAppMessage({ message: \"在庫を確認したいです\" });\n * }\n * }, [init, isReady, sendAppMessage]);\n *\n * return <iframe ref={setIframeEl} src=\"https://example.com/embed\" />;\n * }\n * ```\n */\nexport function useChatbotFrameHandler({\n setOpen,\n setIsExpanded,\n setIsFullScreen,\n}: UseChatbotFrameHandlerProps) {\n const [iframeEl, setIframeEl] = React.useState<HTMLIFrameElement | null>(\n null,\n );\n const [isReady, setIsReady] = React.useState(false);\n const [isInitialized, setIsInitialized] = React.useState(false);\n const pendingAppMessageRef = React.useRef<SendAppMessagePayload | null>(null);\n const clientToolRegistryRef = React.useRef(\n new Map<string, RegisteredClientTool>(),\n );\n\n // helper to post message to the iframe\n const postMessage = React.useCallback(\n (message: ParentMessage) => {\n if (iframeEl?.contentWindow) {\n if (process.env.NODE_ENV === \"development\") {\n console.log(\"sending message to iframe\", message);\n }\n iframeEl.contentWindow.postMessage(message, \"*\");\n return true;\n }\n return false;\n },\n [iframeEl],\n );\n\n const init = React.useCallback(\n (props: InitProps) => {\n postMessage({\n type: \"init\",\n ...props,\n });\n },\n [postMessage],\n );\n\n const setMcps = React.useCallback(\n (mcps: MCP[]) => {\n postMessage({ type: \"mcps\", mcps });\n },\n [postMessage],\n );\n\n const setAppName = React.useCallback(\n (appName: string) => {\n postMessage({ type: \"appName\", appName });\n },\n [postMessage],\n );\n\n const setSessionToken = React.useCallback(\n (sessionToken: string) => {\n postMessage({ type: \"sessionToken\", sessionToken });\n },\n [postMessage],\n );\n\n const flushPendingAppMessage = React.useCallback(() => {\n const pendingPayload = pendingAppMessageRef.current;\n if (!pendingPayload || !isReady || !isInitialized) {\n return false;\n }\n\n const sent = postMessage({\n type: \"appMessage\",\n payload: pendingPayload,\n });\n\n if (sent) {\n pendingAppMessageRef.current = null;\n }\n\n return sent;\n }, [isInitialized, isReady, postMessage]);\n\n const sendAppMessage = React.useCallback(\n (payload: SendAppMessagePayload) => {\n pendingAppMessageRef.current = {\n ...payload,\n startOnNewChat: payload.startOnNewChat ?? true,\n };\n setOpen(true);\n flushPendingAppMessage();\n },\n [flushPendingAppMessage, setOpen],\n );\n\n React.useEffect(() => {\n flushPendingAppMessage();\n }, [flushPendingAppMessage]);\n\n const registerClientTool = React.useCallback(\n (tool: RegisteredClientTool) => {\n clientToolRegistryRef.current.set(tool.definition.name, tool);\n if (!isReady || !isInitialized) {\n return;\n }\n\n postMessage({\n type: \"registerClientTools\",\n tools: [tool.definition],\n });\n },\n [isInitialized, isReady, postMessage],\n );\n\n const unregisterClientTool = React.useCallback(\n (name: string) => {\n const deleted = clientToolRegistryRef.current.delete(name);\n if (!deleted || !isReady || !isInitialized) {\n return;\n }\n\n postMessage({\n type: \"unregisterClientTools\",\n names: [name],\n });\n },\n [isInitialized, isReady, postMessage],\n );\n\n React.useEffect(() => {\n if (!isReady || !isInitialized) {\n return;\n }\n\n const tools = [...clientToolRegistryRef.current.values()].map(\n (tool) => tool.definition,\n );\n if (tools.length === 0) {\n return;\n }\n\n postMessage({\n type: \"registerClientTools\",\n tools,\n });\n }, [isInitialized, isReady, postMessage]);\n\n // Initialize and setup listeners\n React.useEffect(() => {\n const iframe = iframeEl;\n if (!iframe) {\n return;\n }\n\n const handleMessage = (event: MessageEvent<IframeMessage>) => {\n if (event.source !== iframe.contentWindow) {\n return;\n }\n if (process.env.NODE_ENV === \"development\") {\n console.log(\"iframe message\", event.data);\n }\n // Handle iframe messages\n switch (event.data?.type) {\n case \"CHATBOT_READY\":\n setIsReady(true);\n break;\n case \"CHATBOT_INITIALIZED\":\n setIsInitialized(true);\n break;\n case \"CLOSE_CHATBOT\":\n setOpen(false);\n break;\n case \"EXPAND_CHATBOT\":\n setIsExpanded(true);\n break;\n case \"SHRINK_CHATBOT\":\n setIsExpanded(false);\n break;\n case \"FULLSCREEN_CHATBOT\":\n setIsFullScreen(true);\n break;\n case \"EXIT_FULLSCREEN_CHATBOT\":\n setIsFullScreen(false);\n break;\n case \"RELOAD_CHATBOT\":\n // biome-ignore lint/correctness/noSelfAssign: iframeを再読み込みするため\n iframe.src = iframe.src;\n break;\n case \"EXECUTE_CLIENT_TOOL\": {\n const { callId, name, args } = event.data;\n const tool = clientToolRegistryRef.current.get(name);\n if (!tool) {\n postMessage({\n type: \"clientToolResult\",\n callId,\n error: `Client tool not found: ${name}`,\n });\n break;\n }\n\n void (async () => {\n try {\n const result = await tool.execute(args);\n postMessage({\n type: \"clientToolResult\",\n callId,\n result,\n });\n } catch (error) {\n postMessage({\n type: \"clientToolResult\",\n callId,\n error: serializeError(error),\n });\n }\n })();\n break;\n }\n }\n };\n\n const handleIframeLoad = () => {\n setIsReady(false);\n setIsInitialized(false);\n };\n\n window.addEventListener(\"message\", handleMessage);\n iframe.addEventListener(\"load\", handleIframeLoad);\n\n return () => {\n window.removeEventListener(\"message\", handleMessage);\n iframe.removeEventListener(\"load\", handleIframeLoad);\n };\n }, [iframeEl]); // Run when iframe gets mounted\n\n return {\n init,\n setMcps,\n setAppName,\n setSessionToken,\n sendAppMessage,\n registerClientTool,\n unregisterClientTool,\n setIframeEl,\n isReady,\n isInitialized,\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"use-chatbot-frame-handler.mjs","names":[],"sources":["../../../src/components/provider/use-chatbot-frame-handler.tsx"],"sourcesContent":["\"use client\";\n\nimport React from \"react\";\nimport type {\n ClientToolDefinition,\n ClientToolExecuteRequest,\n RegisteredClientTool,\n} from \"../../lib/client-tools\";\n\nexport type IframeMessage =\n | { type: \"CHATBOT_READY\" }\n | { type: \"CHATBOT_INITIALIZED\" }\n | { type: \"CLOSE_CHATBOT\" }\n | { type: \"EXPAND_CHATBOT\" }\n | { type: \"SHRINK_CHATBOT\" }\n | { type: \"FULLSCREEN_CHATBOT\" }\n | { type: \"EXIT_FULLSCREEN_CHATBOT\" }\n | { type: \"RELOAD_CHATBOT\" }\n | ({\n type: \"EXECUTE_CLIENT_TOOL\";\n } & ClientToolExecuteRequest);\n\nexport type MCP = {\n name: string;\n url: string;\n headers?: Record<string, string>;\n};\n\nexport type InitProps = {\n mcps?: MCP[];\n appName?: string;\n sessionToken?: string;\n};\n\nexport type AppMessageAttachment = {\n /** 添付ファイルの表示名です。 */\n fileName: string;\n /** 添付ファイルの MIME type です。 */\n mediaType: string;\n /** 添付ファイルを取得できる URL です。 */\n url: string;\n};\n\nexport type SendAppMessagePayload = {\n /** チャット上に表示するタイトルです。 */\n title?: string;\n /** ユーザーに表示するメッセージ本文です。 */\n message: string;\n /** UI に表示せず、裏側のコンテキストとして渡すメッセージです。 */\n hiddenMessage?: string;\n /** 新規チャットを開始してから送るかを指定します。 */\n startOnNewChat?: boolean;\n /** メッセージと一緒に渡す添付ファイル一覧です。 */\n attachments?: AppMessageAttachment[];\n /** このメッセージ送信で使用するモデルの alias key です。 */\n model?: string;\n};\n\n/**\n * 親(ホストアプリ)から iframe へ送るメッセージ。\n * embed と受信側にも同名の型があるため、追加時は3箇所を揃えること。\n *\n * @see apps/web/src/app/embed/_components/chat-embed.tsx - ParentMessage(受信側)\n * @see packages/embed/src/index.ts - ParentMessage(embed.js)\n */\nexport type ParentMessage =\n | {\n type: \"mcps\";\n mcps: MCP[];\n }\n | {\n type: \"appName\";\n appName: string;\n }\n | {\n type: \"sessionToken\";\n sessionToken: string;\n }\n | {\n type: \"init\";\n mcps?: MCP[];\n appName?: string;\n sessionToken?: string;\n }\n | {\n type: \"appMessage\";\n payload: SendAppMessagePayload;\n }\n | {\n /** 送信はせず、入力欄にテキストを流し込むだけ */\n type: \"setInput\";\n text: string;\n }\n | {\n type: \"registerClientTools\";\n tools: ClientToolDefinition[];\n }\n | {\n type: \"unregisterClientTools\";\n names: string[];\n }\n | {\n type: \"clientToolResult\";\n callId: string;\n result?: unknown;\n error?: string;\n };\n\nfunction serializeError(error: unknown) {\n if (error instanceof Error) {\n return error.message;\n }\n\n if (typeof error === \"string\") {\n return error;\n }\n\n return \"Unknown client tool error\";\n}\n\ntype UseChatbotFrameHandlerProps = {\n /** チャットボットの開閉状態を更新します。 */\n setOpen: (open: boolean) => void;\n /** 展開状態を更新します。 */\n setIsExpanded: (expanded: boolean) => void;\n /** フルスクリーン状態を更新します。 */\n setIsFullScreen: (fullScreen: boolean) => void;\n};\n\n/**\n * iframe と親アプリ間のメッセージ送受信を管理し、チャットボット制御 API をまとめます。\n *\n * @returns iframe 初期化、メッセージ送信、Client Tool 登録に使うハンドラ一式を返します。\n *\n * @example\n * ```tsx\n * export function FrameBridge() {\n * const [open, setOpen] = React.useState(false);\n * const [isExpanded, setIsExpanded] = React.useState(false);\n * const [isFullScreen, setIsFullScreen] = React.useState(false);\n * const { setIframeEl, init, sendAppMessage, isReady } =\n * useChatbotFrameHandler({\n * setOpen,\n * setIsExpanded,\n * setIsFullScreen,\n * });\n *\n * React.useEffect(() => {\n * if (isReady) {\n * init({ appName: \"EC Force\" });\n * sendAppMessage({ message: \"在庫を確認したいです\" });\n * }\n * }, [init, isReady, sendAppMessage]);\n *\n * return <iframe ref={setIframeEl} src=\"https://example.com/embed\" />;\n * }\n * ```\n */\nexport function useChatbotFrameHandler({\n setOpen,\n setIsExpanded,\n setIsFullScreen,\n}: UseChatbotFrameHandlerProps) {\n const [iframeEl, setIframeEl] = React.useState<HTMLIFrameElement | null>(\n null,\n );\n const [isReady, setIsReady] = React.useState(false);\n const [isInitialized, setIsInitialized] = React.useState(false);\n const pendingAppMessageRef = React.useRef<SendAppMessagePayload | null>(null);\n const pendingInputRef = React.useRef<string | null>(null);\n const clientToolRegistryRef = React.useRef(\n new Map<string, RegisteredClientTool>(),\n );\n\n // helper to post message to the iframe\n const postMessage = React.useCallback(\n (message: ParentMessage) => {\n if (iframeEl?.contentWindow) {\n if (process.env.NODE_ENV === \"development\") {\n console.log(\"sending message to iframe\", message);\n }\n iframeEl.contentWindow.postMessage(message, \"*\");\n return true;\n }\n return false;\n },\n [iframeEl],\n );\n\n const init = React.useCallback(\n (props: InitProps) => {\n postMessage({\n type: \"init\",\n ...props,\n });\n },\n [postMessage],\n );\n\n const setMcps = React.useCallback(\n (mcps: MCP[]) => {\n postMessage({ type: \"mcps\", mcps });\n },\n [postMessage],\n );\n\n const setAppName = React.useCallback(\n (appName: string) => {\n postMessage({ type: \"appName\", appName });\n },\n [postMessage],\n );\n\n const setSessionToken = React.useCallback(\n (sessionToken: string) => {\n postMessage({ type: \"sessionToken\", sessionToken });\n },\n [postMessage],\n );\n\n const flushPendingInput = React.useCallback(() => {\n const pendingText = pendingInputRef.current;\n if (pendingText === null || !isReady || !isInitialized) {\n return false;\n }\n\n const sent = postMessage({ type: \"setInput\", text: pendingText });\n\n if (sent) {\n pendingInputRef.current = null;\n }\n\n return sent;\n }, [isInitialized, isReady, postMessage]);\n\n /**\n * 入力欄にテキストを流し込む(送信はしない)。\n *\n * sendAppMessage() と同じく、閉じていればパネルを開き、iframe の準備が\n * 整うまでテキストを保持する。postMessage は iframe 未マウント時に false を\n * 返して破棄されるため、保持しないと「開いた直後に呼ぶと消える」ことになる。\n */\n const setInput = React.useCallback(\n (text: string) => {\n pendingInputRef.current = text;\n setOpen(true);\n flushPendingInput();\n },\n [flushPendingInput, setOpen],\n );\n\n const flushPendingAppMessage = React.useCallback(() => {\n const pendingPayload = pendingAppMessageRef.current;\n if (!pendingPayload || !isReady || !isInitialized) {\n return false;\n }\n\n const sent = postMessage({\n type: \"appMessage\",\n payload: pendingPayload,\n });\n\n if (sent) {\n pendingAppMessageRef.current = null;\n }\n\n return sent;\n }, [isInitialized, isReady, postMessage]);\n\n const sendAppMessage = React.useCallback(\n (payload: SendAppMessagePayload) => {\n pendingAppMessageRef.current = {\n ...payload,\n startOnNewChat: payload.startOnNewChat ?? true,\n };\n setOpen(true);\n flushPendingAppMessage();\n },\n [flushPendingAppMessage, setOpen],\n );\n\n React.useEffect(() => {\n flushPendingInput();\n flushPendingAppMessage();\n }, [flushPendingInput, flushPendingAppMessage]);\n\n const registerClientTool = React.useCallback(\n (tool: RegisteredClientTool) => {\n clientToolRegistryRef.current.set(tool.definition.name, tool);\n if (!isReady || !isInitialized) {\n return;\n }\n\n postMessage({\n type: \"registerClientTools\",\n tools: [tool.definition],\n });\n },\n [isInitialized, isReady, postMessage],\n );\n\n const unregisterClientTool = React.useCallback(\n (name: string) => {\n const deleted = clientToolRegistryRef.current.delete(name);\n if (!deleted || !isReady || !isInitialized) {\n return;\n }\n\n postMessage({\n type: \"unregisterClientTools\",\n names: [name],\n });\n },\n [isInitialized, isReady, postMessage],\n );\n\n React.useEffect(() => {\n if (!isReady || !isInitialized) {\n return;\n }\n\n const tools = [...clientToolRegistryRef.current.values()].map(\n (tool) => tool.definition,\n );\n if (tools.length === 0) {\n return;\n }\n\n postMessage({\n type: \"registerClientTools\",\n tools,\n });\n }, [isInitialized, isReady, postMessage]);\n\n // Initialize and setup listeners\n React.useEffect(() => {\n const iframe = iframeEl;\n if (!iframe) {\n return;\n }\n\n const handleMessage = (event: MessageEvent<IframeMessage>) => {\n if (event.source !== iframe.contentWindow) {\n return;\n }\n if (process.env.NODE_ENV === \"development\") {\n console.log(\"iframe message\", event.data);\n }\n // Handle iframe messages\n switch (event.data?.type) {\n case \"CHATBOT_READY\":\n setIsReady(true);\n break;\n case \"CHATBOT_INITIALIZED\":\n setIsInitialized(true);\n break;\n case \"CLOSE_CHATBOT\":\n setOpen(false);\n break;\n case \"EXPAND_CHATBOT\":\n setIsExpanded(true);\n break;\n case \"SHRINK_CHATBOT\":\n setIsExpanded(false);\n break;\n case \"FULLSCREEN_CHATBOT\":\n setIsFullScreen(true);\n break;\n case \"EXIT_FULLSCREEN_CHATBOT\":\n setIsFullScreen(false);\n break;\n case \"RELOAD_CHATBOT\":\n // biome-ignore lint/correctness/noSelfAssign: iframeを再読み込みするため\n iframe.src = iframe.src;\n break;\n case \"EXECUTE_CLIENT_TOOL\": {\n const { callId, name, args } = event.data;\n const tool = clientToolRegistryRef.current.get(name);\n if (!tool) {\n postMessage({\n type: \"clientToolResult\",\n callId,\n error: `Client tool not found: ${name}`,\n });\n break;\n }\n\n void (async () => {\n try {\n const result = await tool.execute(args);\n postMessage({\n type: \"clientToolResult\",\n callId,\n result,\n });\n } catch (error) {\n postMessage({\n type: \"clientToolResult\",\n callId,\n error: serializeError(error),\n });\n }\n })();\n break;\n }\n }\n };\n\n const handleIframeLoad = () => {\n setIsReady(false);\n setIsInitialized(false);\n };\n\n window.addEventListener(\"message\", handleMessage);\n iframe.addEventListener(\"load\", handleIframeLoad);\n\n return () => {\n window.removeEventListener(\"message\", handleMessage);\n iframe.removeEventListener(\"load\", handleIframeLoad);\n };\n }, [iframeEl]); // Run when iframe gets mounted\n\n return {\n init,\n setMcps,\n setAppName,\n setSessionToken,\n setInput,\n sendAppMessage,\n registerClientTool,\n unregisterClientTool,\n setIframeEl,\n isReady,\n isInitialized,\n };\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -33,6 +33,8 @@ type ChatbotContextType = {
|
|
|
33
33
|
setAppName: (appName: string) => void;
|
|
34
34
|
/** セッショントークンを設定 */
|
|
35
35
|
setSessionToken: (sessionToken: string) => void;
|
|
36
|
+
/** 入力欄にテキストを流し込む(送信はしない) */
|
|
37
|
+
setInput: (text: string) => void;
|
|
36
38
|
/** アプリからメッセージを送信 */
|
|
37
39
|
sendAppMessage: (payload: SendAppMessagePayload) => void;
|
|
38
40
|
/** Client Toolを登録 */
|
|
@@ -108,6 +110,7 @@ export function ChatbotProvider({ children }: ChatbotProviderProps) {
|
|
|
108
110
|
setMcps,
|
|
109
111
|
setAppName,
|
|
110
112
|
setSessionToken,
|
|
113
|
+
setInput,
|
|
111
114
|
sendAppMessage,
|
|
112
115
|
registerClientTool,
|
|
113
116
|
unregisterClientTool,
|
|
@@ -130,6 +133,7 @@ export function ChatbotProvider({ children }: ChatbotProviderProps) {
|
|
|
130
133
|
isFullScreen,
|
|
131
134
|
setIsFullScreen,
|
|
132
135
|
setSessionToken,
|
|
136
|
+
setInput,
|
|
133
137
|
sendAppMessage,
|
|
134
138
|
registerClientTool,
|
|
135
139
|
unregisterClientTool,
|
|
@@ -56,6 +56,13 @@ export type SendAppMessagePayload = {
|
|
|
56
56
|
model?: string;
|
|
57
57
|
};
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* 親(ホストアプリ)から iframe へ送るメッセージ。
|
|
61
|
+
* embed と受信側にも同名の型があるため、追加時は3箇所を揃えること。
|
|
62
|
+
*
|
|
63
|
+
* @see apps/web/src/app/embed/_components/chat-embed.tsx - ParentMessage(受信側)
|
|
64
|
+
* @see packages/embed/src/index.ts - ParentMessage(embed.js)
|
|
65
|
+
*/
|
|
59
66
|
export type ParentMessage =
|
|
60
67
|
| {
|
|
61
68
|
type: "mcps";
|
|
@@ -79,6 +86,11 @@ export type ParentMessage =
|
|
|
79
86
|
type: "appMessage";
|
|
80
87
|
payload: SendAppMessagePayload;
|
|
81
88
|
}
|
|
89
|
+
| {
|
|
90
|
+
/** 送信はせず、入力欄にテキストを流し込むだけ */
|
|
91
|
+
type: "setInput";
|
|
92
|
+
text: string;
|
|
93
|
+
}
|
|
82
94
|
| {
|
|
83
95
|
type: "registerClientTools";
|
|
84
96
|
tools: ClientToolDefinition[];
|
|
@@ -155,6 +167,7 @@ export function useChatbotFrameHandler({
|
|
|
155
167
|
const [isReady, setIsReady] = React.useState(false);
|
|
156
168
|
const [isInitialized, setIsInitialized] = React.useState(false);
|
|
157
169
|
const pendingAppMessageRef = React.useRef<SendAppMessagePayload | null>(null);
|
|
170
|
+
const pendingInputRef = React.useRef<string | null>(null);
|
|
158
171
|
const clientToolRegistryRef = React.useRef(
|
|
159
172
|
new Map<string, RegisteredClientTool>(),
|
|
160
173
|
);
|
|
@@ -205,6 +218,37 @@ export function useChatbotFrameHandler({
|
|
|
205
218
|
[postMessage],
|
|
206
219
|
);
|
|
207
220
|
|
|
221
|
+
const flushPendingInput = React.useCallback(() => {
|
|
222
|
+
const pendingText = pendingInputRef.current;
|
|
223
|
+
if (pendingText === null || !isReady || !isInitialized) {
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const sent = postMessage({ type: "setInput", text: pendingText });
|
|
228
|
+
|
|
229
|
+
if (sent) {
|
|
230
|
+
pendingInputRef.current = null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return sent;
|
|
234
|
+
}, [isInitialized, isReady, postMessage]);
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* 入力欄にテキストを流し込む(送信はしない)。
|
|
238
|
+
*
|
|
239
|
+
* sendAppMessage() と同じく、閉じていればパネルを開き、iframe の準備が
|
|
240
|
+
* 整うまでテキストを保持する。postMessage は iframe 未マウント時に false を
|
|
241
|
+
* 返して破棄されるため、保持しないと「開いた直後に呼ぶと消える」ことになる。
|
|
242
|
+
*/
|
|
243
|
+
const setInput = React.useCallback(
|
|
244
|
+
(text: string) => {
|
|
245
|
+
pendingInputRef.current = text;
|
|
246
|
+
setOpen(true);
|
|
247
|
+
flushPendingInput();
|
|
248
|
+
},
|
|
249
|
+
[flushPendingInput, setOpen],
|
|
250
|
+
);
|
|
251
|
+
|
|
208
252
|
const flushPendingAppMessage = React.useCallback(() => {
|
|
209
253
|
const pendingPayload = pendingAppMessageRef.current;
|
|
210
254
|
if (!pendingPayload || !isReady || !isInitialized) {
|
|
@@ -236,8 +280,9 @@ export function useChatbotFrameHandler({
|
|
|
236
280
|
);
|
|
237
281
|
|
|
238
282
|
React.useEffect(() => {
|
|
283
|
+
flushPendingInput();
|
|
239
284
|
flushPendingAppMessage();
|
|
240
|
-
}, [flushPendingAppMessage]);
|
|
285
|
+
}, [flushPendingInput, flushPendingAppMessage]);
|
|
241
286
|
|
|
242
287
|
const registerClientTool = React.useCallback(
|
|
243
288
|
(tool: RegisteredClientTool) => {
|
|
@@ -380,6 +425,7 @@ export function useChatbotFrameHandler({
|
|
|
380
425
|
setMcps,
|
|
381
426
|
setAppName,
|
|
382
427
|
setSessionToken,
|
|
428
|
+
setInput,
|
|
383
429
|
sendAppMessage,
|
|
384
430
|
registerClientTool,
|
|
385
431
|
unregisterClientTool,
|