@super_studio/ecforce-ai-agent-react 1.1.2 → 1.2.0-canary.1

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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client-tools.d.mts","names":[],"sources":["../../src/lib/client-tools.ts"],"sourcesContent":[],"mappings":";;;KAOY,oBAAA,GAAuB;KAGvB,oBAAA;EAHA,IAAA,EAAA,MAAA;EAGA,WAAA,EAAA,MAAA;EASA,WAAA,EAAA,MAAA;EAMA,OAAA,CAAA,EAAA,MAAA;EAKA,mBAAA,CAAA,EAAA,OAAoB;EAAuB,UAAA,EAdzC,oBAcyC;CAYzC;AAEa,KAzBf,wBAAA,GAyBe;EAAP,MAAA,EAAA,MAAA;EAAwB,IAAA,EAAA,MAAA;EAAO,IAAA,EAAA,OAAA;AAiEnD,CAAA;AAAoD,KApFxC,oBAAA,GAoFwC;EAClD,UAAA,EApFY,oBAoFZ;EACA,OAAA,EAAA,CAAA,IAAA,EAAA,OAAA,EAAA,GApF4B,OAoF5B,CAAA,OAAA,CAAA,GAAA,OAAA;CACA;AACA,KAnFU,oBAmFV,CAAA,oBAnFmD,CAAA,CAAE,UAmFrD,CAAA,GAAA;EACA;EACA,IAAA,EAAA,MAAA;EACA;EACsB,WAAA,EAAA,MAAA;EAArB;EAAoB,WAAA,EAAA,MAAA;;;;;;cA3ET;;kBAEI,CAAA,CAAE,OAAO,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAiE5B,kCAAkC,CAAA,CAAE;;;;;;;;GAQjD,qBAAqB"}
@@ -0,0 +1,91 @@
1
+ "use client";
2
+
3
+ import { useChatbot } from "../components/provider/chatbot-provider.mjs";
4
+ import React from "react";
5
+ import { zodToJsonSchema } from "zod-to-json-schema";
6
+
7
+ //#region src/lib/client-tools.ts
8
+ const CLIENT_TOOL_NAME_PREFIX = "page_";
9
+ function normalizeClientToolName(name) {
10
+ if (name.startsWith(CLIENT_TOOL_NAME_PREFIX)) return name;
11
+ return `${CLIENT_TOOL_NAME_PREFIX}${name}`;
12
+ }
13
+ function isZodV4Schema(parameters) {
14
+ return "_zod" in parameters;
15
+ }
16
+ function parametersToJsonSchema(parameters, zodModule) {
17
+ if (zodModule && isZodV4Schema(parameters)) {
18
+ const nativeToJsonSchema = zodModule.z.toJSONSchema;
19
+ if (typeof nativeToJsonSchema === "function") return nativeToJsonSchema(parameters);
20
+ }
21
+ return zodToJsonSchema(parameters, { $refStrategy: "none" });
22
+ }
23
+ /**
24
+ * チャットボットから呼び出せるクライアントツールを登録します。
25
+ *
26
+ * @example
27
+ * ```tsx
28
+ * import { z } from "zod";
29
+ * import { useClientTool } from "@super_studio/ecforce-ai-agent-react";
30
+ *
31
+ * export function InventoryToolRegistration() {
32
+ * useClientTool({
33
+ * name: "lookup_inventory",
34
+ * displayName: "在庫確認",
35
+ * description: "商品コードから在庫数を取得します。",
36
+ * summary: "商品在庫を返します。",
37
+ * parameters: z.object({
38
+ * sku: z.string().describe("商品コード"),
39
+ * }),
40
+ * execute: async ({ sku }) => {
41
+ * return { sku, available: true };
42
+ * },
43
+ * });
44
+ *
45
+ * return null;
46
+ * }
47
+ * ```
48
+ */
49
+ function useClientTool({ name, displayName, description, summary, requireConfirmation = false, parameters, execute }) {
50
+ const { registerClientTool, unregisterClientTool } = useChatbot();
51
+ const executeRef = React.useRef(execute);
52
+ const normalizedName = React.useMemo(() => normalizeClientToolName(name), [name]);
53
+ React.useEffect(() => {
54
+ executeRef.current = execute;
55
+ }, [execute]);
56
+ React.useEffect(() => {
57
+ let isCancelled = false;
58
+ (async () => {
59
+ const zodModule = isZodV4Schema(parameters) ? await import("zod") : void 0;
60
+ if (isCancelled) return;
61
+ registerClientTool({
62
+ definition: {
63
+ name: normalizedName,
64
+ displayName,
65
+ description,
66
+ summary,
67
+ requireConfirmation,
68
+ parameters: parametersToJsonSchema(parameters, zodModule)
69
+ },
70
+ execute: (args) => executeRef.current(args)
71
+ });
72
+ })();
73
+ return () => {
74
+ isCancelled = true;
75
+ unregisterClientTool(normalizedName);
76
+ };
77
+ }, [
78
+ description,
79
+ displayName,
80
+ normalizedName,
81
+ parameters,
82
+ registerClientTool,
83
+ requireConfirmation,
84
+ summary,
85
+ unregisterClientTool
86
+ ]);
87
+ }
88
+
89
+ //#endregion
90
+ export { useClientTool };
91
+ //# sourceMappingURL=client-tools.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client-tools.mjs","names":[],"sources":["../../src/lib/client-tools.ts"],"sourcesContent":["\"use client\";\n\nimport React from \"react\";\nimport type { z } from \"zod\";\nimport { zodToJsonSchema } from \"zod-to-json-schema\";\nimport { useChatbot } from \"../components/provider/chatbot-provider\";\n\nexport type ClientToolJsonSchema = Record<string, unknown>;\nconst CLIENT_TOOL_NAME_PREFIX = \"page_\";\n\nexport type ClientToolDefinition = {\n name: string;\n displayName: string;\n description: string;\n summary?: string;\n requireConfirmation?: boolean;\n parameters: ClientToolJsonSchema;\n};\n\nexport type ClientToolExecuteRequest = {\n callId: string;\n name: string;\n args: unknown;\n};\n\nexport type RegisteredClientTool = {\n definition: ClientToolDefinition;\n execute: (args: unknown) => Promise<unknown> | unknown;\n};\n\nexport type UseClientToolOptions<TParameters extends z.ZodTypeAny> = {\n /** ツール名です。`page_`のプレフィックスが自動で追加されます。 */\n name: string;\n /** UI上に表示するツール名です。 */\n displayName: string;\n /** LLMが見れるツールの詳細説明です。 */\n description: string;\n /** ユーザーに表示するツールの要約です。 */\n summary?: string;\n /** 実行前にユーザー確認を要求するかを指定します。 */\n requireConfirmation?: boolean;\n /** ツールの引数を定義するZodスキーマです。 */\n parameters: TParameters;\n /** 検証済み引数を受け取り、ツール本体の処理を実行します。 */\n execute: (args: z.output<TParameters>) => Promise<unknown> | unknown;\n};\n\nfunction normalizeClientToolName(name: string) {\n if (name.startsWith(CLIENT_TOOL_NAME_PREFIX)) {\n return name;\n }\n\n return `${CLIENT_TOOL_NAME_PREFIX}${name}`;\n}\n\nfunction isZodV4Schema(parameters: z.ZodTypeAny) {\n return \"_zod\" in (parameters as object);\n}\n\nfunction parametersToJsonSchema(\n parameters: z.ZodTypeAny,\n zodModule?: typeof import(\"zod\"),\n): ClientToolJsonSchema {\n if (zodModule && isZodV4Schema(parameters)) {\n const nativeToJsonSchema = (\n zodModule.z as typeof zodModule.z & {\n toJSONSchema?: (schema: z.ZodTypeAny) => ClientToolJsonSchema;\n }\n ).toJSONSchema;\n\n if (typeof nativeToJsonSchema === \"function\") {\n return nativeToJsonSchema(parameters);\n }\n }\n\n return zodToJsonSchema(\n parameters as unknown as Parameters<typeof zodToJsonSchema>[0],\n {\n $refStrategy: \"none\",\n },\n ) as ClientToolJsonSchema;\n}\n\n/**\n * チャットボットから呼び出せるクライアントツールを登録します。\n *\n * @example\n * ```tsx\n * import { z } from \"zod\";\n * import { useClientTool } from \"@super_studio/ecforce-ai-agent-react\";\n *\n * export function InventoryToolRegistration() {\n * useClientTool({\n * name: \"lookup_inventory\",\n * displayName: \"在庫確認\",\n * description: \"商品コードから在庫数を取得します。\",\n * summary: \"商品在庫を返します。\",\n * parameters: z.object({\n * sku: z.string().describe(\"商品コード\"),\n * }),\n * execute: async ({ sku }) => {\n * return { sku, available: true };\n * },\n * });\n *\n * return null;\n * }\n * ```\n */\nexport function useClientTool<TParameters extends z.ZodTypeAny>({\n name,\n displayName,\n description,\n summary,\n requireConfirmation = false,\n parameters,\n execute,\n}: UseClientToolOptions<TParameters>) {\n const { registerClientTool, unregisterClientTool } = useChatbot();\n const executeRef = React.useRef(execute);\n const normalizedName = React.useMemo(\n () => normalizeClientToolName(name),\n [name],\n );\n\n React.useEffect(() => {\n executeRef.current = execute;\n }, [execute]);\n\n React.useEffect(() => {\n let isCancelled = false;\n\n void (async () => {\n const zodModule = isZodV4Schema(parameters) ? await import(\"zod\") : undefined;\n if (isCancelled) {\n return;\n }\n\n registerClientTool({\n definition: {\n name: normalizedName,\n displayName,\n description,\n summary,\n requireConfirmation,\n parameters: parametersToJsonSchema(parameters, zodModule),\n },\n execute: (args) => executeRef.current(args as z.output<TParameters>),\n });\n })();\n\n return () => {\n isCancelled = true;\n unregisterClientTool(normalizedName);\n };\n }, [\n description,\n displayName,\n normalizedName,\n parameters,\n registerClientTool,\n requireConfirmation,\n summary,\n unregisterClientTool,\n ]);\n}\n"],"mappings":";;;;;;;AAQA,MAAM,0BAA0B;AAuChC,SAAS,wBAAwB,MAAc;AAC7C,KAAI,KAAK,WAAW,wBAAwB,CAC1C,QAAO;AAGT,QAAO,GAAG,0BAA0B;;AAGtC,SAAS,cAAc,YAA0B;AAC/C,QAAO,UAAW;;AAGpB,SAAS,uBACP,YACA,WACsB;AACtB,KAAI,aAAa,cAAc,WAAW,EAAE;EAC1C,MAAM,qBACJ,UAAU,EAGV;AAEF,MAAI,OAAO,uBAAuB,WAChC,QAAO,mBAAmB,WAAW;;AAIzC,QAAO,gBACL,YACA,EACE,cAAc,QACf,CACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BH,SAAgB,cAAgD,EAC9D,MACA,aACA,aACA,SACA,sBAAsB,OACtB,YACA,WACoC;CACpC,MAAM,EAAE,oBAAoB,yBAAyB,YAAY;CACjE,MAAM,aAAa,MAAM,OAAO,QAAQ;CACxC,MAAM,iBAAiB,MAAM,cACrB,wBAAwB,KAAK,EACnC,CAAC,KAAK,CACP;AAED,OAAM,gBAAgB;AACpB,aAAW,UAAU;IACpB,CAAC,QAAQ,CAAC;AAEb,OAAM,gBAAgB;EACpB,IAAI,cAAc;AAElB,GAAM,YAAY;GAChB,MAAM,YAAY,cAAc,WAAW,GAAG,MAAM,OAAO,SAAS;AACpE,OAAI,YACF;AAGF,sBAAmB;IACjB,YAAY;KACV,MAAM;KACN;KACA;KACA;KACA;KACA,YAAY,uBAAuB,YAAY,UAAU;KAC1D;IACD,UAAU,SAAS,WAAW,QAAQ,KAA8B;IACrE,CAAC;MACA;AAEJ,eAAa;AACX,iBAAc;AACd,wBAAqB,eAAe;;IAErC;EACD;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACD,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@super_studio/ecforce-ai-agent-react",
3
- "version": "1.1.2",
3
+ "version": "1.2.0-canary.1",
4
4
  "main": "./dist/index.mjs",
5
5
  "module": "./dist/index.mjs",
6
6
  "types": "./dist/index.d.mts",
@@ -20,18 +20,26 @@
20
20
  },
21
21
  "dependencies": {
22
22
  "@radix-ui/react-dialog": "1.1.14",
23
- "@radix-ui/react-tooltip": "1.2.7"
23
+ "@radix-ui/react-tooltip": "1.2.7",
24
+ "zod-to-json-schema": "3.25.2"
24
25
  },
25
26
  "peerDependencies": {
26
27
  "react": ">=16",
27
- "react-dom": ">=16"
28
+ "react-dom": ">=16",
29
+ "zod": ">=3"
30
+ },
31
+ "peerDependenciesMeta": {
32
+ "zod": {
33
+ "optional": true
34
+ }
28
35
  },
29
36
  "devDependencies": {
30
37
  "@types/node": "22.14.0",
31
38
  "@types/react": "19.2.7",
32
39
  "@types/react-dom": "19.2.3",
33
40
  "react": "19.2.4",
34
- "react-dom": "19.2.4"
41
+ "react-dom": "19.2.4",
42
+ "zod": "4.1.12"
35
43
  },
36
44
  "scripts": {
37
45
  "dev": "tsdown --watch",
@@ -1,11 +1,12 @@
1
1
  "use client";
2
2
 
3
3
  import React from "react";
4
+ import type { RegisteredClientTool } from "../../lib/client-tools";
4
5
  import {
5
- type SendAppMessagePayload,
6
6
  useChatbotFrameHandler,
7
7
  type InitProps,
8
8
  type MCP,
9
+ type SendAppMessagePayload,
9
10
  } from "./use-chatbot-frame-handler";
10
11
  import { useChatbotWindowStates } from "./use-chatbot-window-states";
11
12
 
@@ -34,6 +35,10 @@ type ChatbotContextType = {
34
35
  setSessionToken: (sessionToken: string) => void;
35
36
  /** アプリからメッセージを送信 */
36
37
  sendAppMessage: (payload: SendAppMessagePayload) => void;
38
+ /** Client Toolを登録 */
39
+ registerClientTool: (tool: RegisteredClientTool) => void;
40
+ /** Client Toolを解除 */
41
+ unregisterClientTool: (name: string) => void;
37
42
  /** チャットボットが準備できたか */
38
43
  isReady: boolean;
39
44
  /** チャットボットが初期化されたか */
@@ -49,6 +54,34 @@ const ChatbotContext = React.createContext<ChatbotContextType | undefined>(
49
54
  undefined,
50
55
  );
51
56
 
57
+ /**
58
+ * `ChatbotProvider` が提供するチャットボット状態と操作 API を取得します。
59
+ *
60
+ * @returns チャットボットの表示状態、初期化 API、Client Tool の登録 API を含むコンテキスト値。
61
+ *
62
+ * @example
63
+ * ```tsx
64
+ * import { useChatbot } from "@super_studio/ecforce-ai-agent-react";
65
+ *
66
+ * export function OpenChatButton() {
67
+ * const { open, setOpen, sendAppMessage } = useChatbot();
68
+ *
69
+ * return (
70
+ * <button
71
+ * type="button"
72
+ * onClick={() => {
73
+ * setOpen(!open);
74
+ * sendAppMessage({
75
+ * message: "EC Forceの注文状況を確認したいです",
76
+ * });
77
+ * }}
78
+ * >
79
+ * Open chat
80
+ * </button>
81
+ * );
82
+ * }
83
+ * ```
84
+ */
52
85
  export function useChatbot() {
53
86
  const context = React.useContext(ChatbotContext);
54
87
  if (!context) {
@@ -76,6 +109,8 @@ export function ChatbotProvider({ children }: ChatbotProviderProps) {
76
109
  setAppName,
77
110
  setSessionToken,
78
111
  sendAppMessage,
112
+ registerClientTool,
113
+ unregisterClientTool,
79
114
  init,
80
115
  isReady,
81
116
  isInitialized,
@@ -96,6 +131,8 @@ export function ChatbotProvider({ children }: ChatbotProviderProps) {
96
131
  setIsFullScreen,
97
132
  setSessionToken,
98
133
  sendAppMessage,
134
+ registerClientTool,
135
+ unregisterClientTool,
99
136
  init,
100
137
  setMcps,
101
138
  setAppName,
@@ -1,6 +1,11 @@
1
1
  "use client";
2
2
 
3
3
  import React from "react";
4
+ import type {
5
+ ClientToolDefinition,
6
+ ClientToolExecuteRequest,
7
+ RegisteredClientTool,
8
+ } from "../../lib/client-tools";
4
9
 
5
10
  export type IframeMessage =
6
11
  | { type: "CHATBOT_READY" }
@@ -10,7 +15,10 @@ export type IframeMessage =
10
15
  | { type: "SHRINK_CHATBOT" }
11
16
  | { type: "FULLSCREEN_CHATBOT" }
12
17
  | { type: "EXIT_FULLSCREEN_CHATBOT" }
13
- | { type: "RELOAD_CHATBOT" };
18
+ | { type: "RELOAD_CHATBOT" }
19
+ | ({
20
+ type: "EXECUTE_CLIENT_TOOL";
21
+ } & ClientToolExecuteRequest);
14
22
 
15
23
  export type MCP = {
16
24
  name: string;
@@ -25,16 +33,24 @@ export type InitProps = {
25
33
  };
26
34
 
27
35
  export type AppMessageAttachment = {
36
+ /** 添付ファイルの表示名です。 */
28
37
  fileName: string;
38
+ /** 添付ファイルの MIME type です。 */
29
39
  mediaType: string;
40
+ /** 添付ファイルを取得できる URL です。 */
30
41
  url: string;
31
42
  };
32
43
 
33
44
  export type SendAppMessagePayload = {
45
+ /** チャット上に表示するタイトルです。 */
34
46
  title?: string;
47
+ /** ユーザーに表示するメッセージ本文です。 */
35
48
  message: string;
49
+ /** UI に表示せず、裏側のコンテキストとして渡すメッセージです。 */
36
50
  hiddenMessage?: string;
51
+ /** 新規チャットを開始してから送るかを指定します。 */
37
52
  startOnNewChat?: boolean;
53
+ /** メッセージと一緒に渡す添付ファイル一覧です。 */
38
54
  attachments?: AppMessageAttachment[];
39
55
  };
40
56
 
@@ -60,23 +76,86 @@ export type ParentMessage =
60
76
  | {
61
77
  type: "appMessage";
62
78
  payload: SendAppMessagePayload;
79
+ }
80
+ | {
81
+ type: "registerClientTools";
82
+ tools: ClientToolDefinition[];
83
+ }
84
+ | {
85
+ type: "unregisterClientTools";
86
+ names: string[];
87
+ }
88
+ | {
89
+ type: "clientToolResult";
90
+ callId: string;
91
+ result?: unknown;
92
+ error?: string;
63
93
  };
64
94
 
95
+ function serializeError(error: unknown) {
96
+ if (error instanceof Error) {
97
+ return error.message;
98
+ }
99
+
100
+ if (typeof error === "string") {
101
+ return error;
102
+ }
103
+
104
+ return "Unknown client tool error";
105
+ }
106
+
107
+ type UseChatbotFrameHandlerProps = {
108
+ /** チャットボットの開閉状態を更新します。 */
109
+ setOpen: (open: boolean) => void;
110
+ /** 展開状態を更新します。 */
111
+ setIsExpanded: (expanded: boolean) => void;
112
+ /** フルスクリーン状態を更新します。 */
113
+ setIsFullScreen: (fullScreen: boolean) => void;
114
+ };
115
+
116
+ /**
117
+ * iframe と親アプリ間のメッセージ送受信を管理し、チャットボット制御 API をまとめます。
118
+ *
119
+ * @returns iframe 初期化、メッセージ送信、Client Tool 登録に使うハンドラ一式を返します。
120
+ *
121
+ * @example
122
+ * ```tsx
123
+ * export function FrameBridge() {
124
+ * const [open, setOpen] = React.useState(false);
125
+ * const [isExpanded, setIsExpanded] = React.useState(false);
126
+ * const [isFullScreen, setIsFullScreen] = React.useState(false);
127
+ * const { setIframeEl, init, sendAppMessage, isReady } =
128
+ * useChatbotFrameHandler({
129
+ * setOpen,
130
+ * setIsExpanded,
131
+ * setIsFullScreen,
132
+ * });
133
+ *
134
+ * React.useEffect(() => {
135
+ * if (isReady) {
136
+ * init({ appName: "EC Force" });
137
+ * sendAppMessage({ message: "在庫を確認したいです" });
138
+ * }
139
+ * }, [init, isReady, sendAppMessage]);
140
+ *
141
+ * return <iframe ref={setIframeEl} src="https://example.com/embed" />;
142
+ * }
143
+ * ```
144
+ */
65
145
  export function useChatbotFrameHandler({
66
146
  setOpen,
67
147
  setIsExpanded,
68
148
  setIsFullScreen,
69
- }: {
70
- setOpen: (open: boolean) => void;
71
- setIsExpanded: (expanded: boolean) => void;
72
- setIsFullScreen: (fullScreen: boolean) => void;
73
- }) {
149
+ }: UseChatbotFrameHandlerProps) {
74
150
  const [iframeEl, setIframeEl] = React.useState<HTMLIFrameElement | null>(
75
151
  null,
76
152
  );
77
153
  const [isReady, setIsReady] = React.useState(false);
78
154
  const [isInitialized, setIsInitialized] = React.useState(false);
79
155
  const pendingAppMessageRef = React.useRef<SendAppMessagePayload | null>(null);
156
+ const clientToolRegistryRef = React.useRef(
157
+ new Map<string, RegisteredClientTool>(),
158
+ );
80
159
 
81
160
  // helper to post message to the iframe
82
161
  const postMessage = React.useCallback(
@@ -158,6 +237,54 @@ export function useChatbotFrameHandler({
158
237
  flushPendingAppMessage();
159
238
  }, [flushPendingAppMessage]);
160
239
 
240
+ const registerClientTool = React.useCallback(
241
+ (tool: RegisteredClientTool) => {
242
+ clientToolRegistryRef.current.set(tool.definition.name, tool);
243
+ if (!isReady || !isInitialized) {
244
+ return;
245
+ }
246
+
247
+ postMessage({
248
+ type: "registerClientTools",
249
+ tools: [tool.definition],
250
+ });
251
+ },
252
+ [isInitialized, isReady, postMessage],
253
+ );
254
+
255
+ const unregisterClientTool = React.useCallback(
256
+ (name: string) => {
257
+ const deleted = clientToolRegistryRef.current.delete(name);
258
+ if (!deleted || !isReady || !isInitialized) {
259
+ return;
260
+ }
261
+
262
+ postMessage({
263
+ type: "unregisterClientTools",
264
+ names: [name],
265
+ });
266
+ },
267
+ [isInitialized, isReady, postMessage],
268
+ );
269
+
270
+ React.useEffect(() => {
271
+ if (!isReady || !isInitialized) {
272
+ return;
273
+ }
274
+
275
+ const tools = [...clientToolRegistryRef.current.values()].map(
276
+ (tool) => tool.definition,
277
+ );
278
+ if (tools.length === 0) {
279
+ return;
280
+ }
281
+
282
+ postMessage({
283
+ type: "registerClientTools",
284
+ tools,
285
+ });
286
+ }, [isInitialized, isReady, postMessage]);
287
+
161
288
  // Initialize and setup listeners
162
289
  React.useEffect(() => {
163
290
  const iframe = iframeEl;
@@ -199,6 +326,36 @@ export function useChatbotFrameHandler({
199
326
  // reload iframe
200
327
  iframe.src = iframe.src;
201
328
  break;
329
+ case "EXECUTE_CLIENT_TOOL": {
330
+ const { callId, name, args } = event.data;
331
+ const tool = clientToolRegistryRef.current.get(name);
332
+ if (!tool) {
333
+ postMessage({
334
+ type: "clientToolResult",
335
+ callId,
336
+ error: `Client tool not found: ${name}`,
337
+ });
338
+ break;
339
+ }
340
+
341
+ void (async () => {
342
+ try {
343
+ const result = await tool.execute(args);
344
+ postMessage({
345
+ type: "clientToolResult",
346
+ callId,
347
+ result,
348
+ });
349
+ } catch (error) {
350
+ postMessage({
351
+ type: "clientToolResult",
352
+ callId,
353
+ error: serializeError(error),
354
+ });
355
+ }
356
+ })();
357
+ break;
358
+ }
202
359
  }
203
360
  };
204
361
 
@@ -222,6 +379,8 @@ export function useChatbotFrameHandler({
222
379
  setAppName,
223
380
  setSessionToken,
224
381
  sendAppMessage,
382
+ registerClientTool,
383
+ unregisterClientTool,
225
384
  setIframeEl,
226
385
  isReady,
227
386
  isInitialized,
@@ -2,6 +2,31 @@
2
2
 
3
3
  import React from "react";
4
4
 
5
+ /**
6
+ * チャットボット UI の開閉・展開・フルスクリーン状態を管理します。
7
+ *
8
+ * @returns 初回オープン判定と、各表示状態、その更新関数を返します。
9
+ *
10
+ * @example
11
+ * ```tsx
12
+ * export function ChatbotWindowController() {
13
+ * const { open, setOpen, isExpanded, setIsExpanded } =
14
+ * useChatbotWindowStates();
15
+ *
16
+ * return (
17
+ * <button
18
+ * type="button"
19
+ * onClick={() => {
20
+ * setOpen(!open);
21
+ * setIsExpanded(!isExpanded);
22
+ * }}
23
+ * >
24
+ * Toggle window
25
+ * </button>
26
+ * );
27
+ * }
28
+ * ```
29
+ */
5
30
  export function useChatbotWindowStates() {
6
31
  const [hasOpened, setHasOpened] = React.useState(false);
7
32
  const [open, setOpen] = React.useState(false);
package/src/index.ts CHANGED
@@ -3,3 +3,4 @@ export * from "./components/chatbot-frame";
3
3
  export * from "./components/chatbot-sheet";
4
4
  export * from "./components/sheet";
5
5
  export * from "./components/tooltip";
6
+ export * from "./lib/client-tools";
@@ -0,0 +1,166 @@
1
+ "use client";
2
+
3
+ import React from "react";
4
+ import type { z } from "zod";
5
+ import { zodToJsonSchema } from "zod-to-json-schema";
6
+ import { useChatbot } from "../components/provider/chatbot-provider";
7
+
8
+ export type ClientToolJsonSchema = Record<string, unknown>;
9
+ const CLIENT_TOOL_NAME_PREFIX = "page_";
10
+
11
+ export type ClientToolDefinition = {
12
+ name: string;
13
+ displayName: string;
14
+ description: string;
15
+ summary?: string;
16
+ requireConfirmation?: boolean;
17
+ parameters: ClientToolJsonSchema;
18
+ };
19
+
20
+ export type ClientToolExecuteRequest = {
21
+ callId: string;
22
+ name: string;
23
+ args: unknown;
24
+ };
25
+
26
+ export type RegisteredClientTool = {
27
+ definition: ClientToolDefinition;
28
+ execute: (args: unknown) => Promise<unknown> | unknown;
29
+ };
30
+
31
+ export type UseClientToolOptions<TParameters extends z.ZodTypeAny> = {
32
+ /** ツール名です。`page_`のプレフィックスが自動で追加されます。 */
33
+ name: string;
34
+ /** UI上に表示するツール名です。 */
35
+ displayName: string;
36
+ /** LLMが見れるツールの詳細説明です。 */
37
+ description: string;
38
+ /** ユーザーに表示するツールの要約です。 */
39
+ summary?: string;
40
+ /** 実行前にユーザー確認を要求するかを指定します。 */
41
+ requireConfirmation?: boolean;
42
+ /** ツールの引数を定義するZodスキーマです。 */
43
+ parameters: TParameters;
44
+ /** 検証済み引数を受け取り、ツール本体の処理を実行します。 */
45
+ execute: (args: z.output<TParameters>) => Promise<unknown> | unknown;
46
+ };
47
+
48
+ function normalizeClientToolName(name: string) {
49
+ if (name.startsWith(CLIENT_TOOL_NAME_PREFIX)) {
50
+ return name;
51
+ }
52
+
53
+ return `${CLIENT_TOOL_NAME_PREFIX}${name}`;
54
+ }
55
+
56
+ function isZodV4Schema(parameters: z.ZodTypeAny) {
57
+ return "_zod" in (parameters as object);
58
+ }
59
+
60
+ function parametersToJsonSchema(
61
+ parameters: z.ZodTypeAny,
62
+ zodModule?: typeof import("zod"),
63
+ ): ClientToolJsonSchema {
64
+ if (zodModule && isZodV4Schema(parameters)) {
65
+ const nativeToJsonSchema = (
66
+ zodModule.z as typeof zodModule.z & {
67
+ toJSONSchema?: (schema: z.ZodTypeAny) => ClientToolJsonSchema;
68
+ }
69
+ ).toJSONSchema;
70
+
71
+ if (typeof nativeToJsonSchema === "function") {
72
+ return nativeToJsonSchema(parameters);
73
+ }
74
+ }
75
+
76
+ return zodToJsonSchema(
77
+ parameters as unknown as Parameters<typeof zodToJsonSchema>[0],
78
+ {
79
+ $refStrategy: "none",
80
+ },
81
+ ) as ClientToolJsonSchema;
82
+ }
83
+
84
+ /**
85
+ * チャットボットから呼び出せるクライアントツールを登録します。
86
+ *
87
+ * @example
88
+ * ```tsx
89
+ * import { z } from "zod";
90
+ * import { useClientTool } from "@super_studio/ecforce-ai-agent-react";
91
+ *
92
+ * export function InventoryToolRegistration() {
93
+ * useClientTool({
94
+ * name: "lookup_inventory",
95
+ * displayName: "在庫確認",
96
+ * description: "商品コードから在庫数を取得します。",
97
+ * summary: "商品在庫を返します。",
98
+ * parameters: z.object({
99
+ * sku: z.string().describe("商品コード"),
100
+ * }),
101
+ * execute: async ({ sku }) => {
102
+ * return { sku, available: true };
103
+ * },
104
+ * });
105
+ *
106
+ * return null;
107
+ * }
108
+ * ```
109
+ */
110
+ export function useClientTool<TParameters extends z.ZodTypeAny>({
111
+ name,
112
+ displayName,
113
+ description,
114
+ summary,
115
+ requireConfirmation = false,
116
+ parameters,
117
+ execute,
118
+ }: UseClientToolOptions<TParameters>) {
119
+ const { registerClientTool, unregisterClientTool } = useChatbot();
120
+ const executeRef = React.useRef(execute);
121
+ const normalizedName = React.useMemo(
122
+ () => normalizeClientToolName(name),
123
+ [name],
124
+ );
125
+
126
+ React.useEffect(() => {
127
+ executeRef.current = execute;
128
+ }, [execute]);
129
+
130
+ React.useEffect(() => {
131
+ let isCancelled = false;
132
+
133
+ void (async () => {
134
+ const zodModule = isZodV4Schema(parameters) ? await import("zod") : undefined;
135
+ if (isCancelled) {
136
+ return;
137
+ }
138
+
139
+ registerClientTool({
140
+ definition: {
141
+ name: normalizedName,
142
+ displayName,
143
+ description,
144
+ summary,
145
+ requireConfirmation,
146
+ parameters: parametersToJsonSchema(parameters, zodModule),
147
+ },
148
+ execute: (args) => executeRef.current(args as z.output<TParameters>),
149
+ });
150
+ })();
151
+
152
+ return () => {
153
+ isCancelled = true;
154
+ unregisterClientTool(normalizedName);
155
+ };
156
+ }, [
157
+ description,
158
+ displayName,
159
+ normalizedName,
160
+ parameters,
161
+ registerClientTool,
162
+ requireConfirmation,
163
+ summary,
164
+ unregisterClientTool,
165
+ ]);
166
+ }