@lantos1618/better-ui 0.9.0 → 0.9.2

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.
@@ -425,6 +425,7 @@ function ChatProvider({ endpoint = "/api/chat", tools, toolStateStore: externalS
425
425
 
426
426
  // src/components/Thread.tsx
427
427
  var import_react6 = require("react");
428
+ var import_lucide_react = require("lucide-react");
428
429
 
429
430
  // src/components/Message.tsx
430
431
  var import_ai2 = require("ai");
@@ -831,18 +832,29 @@ function Thread({ className, emptyMessage, suggestions }) {
831
832
  }
832
833
  }, [messages, isLoading]);
833
834
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { ref: scrollRef, className: `${className || ""}`, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "p-6 space-y-6", children: [
834
- messages.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "text-center py-16", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "text-[var(--bui-fg-faint,#52525b)] text-sm", children: [
835
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "text-[var(--bui-fg-secondary,#a1a1aa)] mb-4", children: emptyMessage || "Send a message to get started" }),
836
- suggestions && suggestions.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "flex flex-wrap justify-center gap-2 mt-4", children: suggestions.map((suggestion) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
835
+ messages.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "py-12", children: [
836
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "text-center text-sm text-[var(--bui-fg-secondary,#a1a1aa)]", children: emptyMessage || "Send a message to get started" }),
837
+ suggestions && suggestions.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "mt-8 flex flex-col divide-y divide-[var(--bui-border,#27272a)] border-y border-[var(--bui-border,#27272a)]", children: suggestions.map((suggestion) => /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
837
838
  "button",
838
839
  {
839
840
  onClick: () => sendMessage(suggestion),
840
- className: "px-3 py-1.5 text-xs text-[var(--bui-fg-secondary,#a1a1aa)] bg-[var(--bui-bg-elevated,#27272a)] border border-[var(--bui-border-strong,#3f3f46)] rounded-full hover:bg-[var(--bui-bg-hover,#3f3f46)] hover:text-[var(--bui-fg,#f4f4f5)] transition-colors",
841
- children: suggestion
841
+ className: "group flex items-center justify-between gap-4 px-5 py-4 text-left text-sm text-[var(--bui-fg,#f4f4f5)] transition-colors hover:bg-[var(--bui-bg-hover,#27272a)]",
842
+ children: [
843
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "flex-1", children: suggestion }),
844
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
845
+ import_lucide_react.ArrowRight,
846
+ {
847
+ size: 14,
848
+ strokeWidth: 1.75,
849
+ "aria-hidden": "true",
850
+ className: "shrink-0 text-[var(--bui-fg-faint,#52525b)] transition-all group-hover:translate-x-0.5 group-hover:text-[var(--bui-fg-secondary,#a1a1aa)]"
851
+ }
852
+ )
853
+ ]
842
854
  },
843
855
  suggestion
844
856
  )) })
845
- ] }) }),
857
+ ] }),
846
858
  messages.map((message, i) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
847
859
  Message,
848
860
  {
@@ -865,6 +877,7 @@ function Thread({ className, emptyMessage, suggestions }) {
865
877
 
866
878
  // src/components/Composer.tsx
867
879
  var import_react7 = require("react");
880
+ var import_lucide_react2 = require("lucide-react");
868
881
  var import_jsx_runtime6 = require("react/jsx-runtime");
869
882
  function Composer({ className, placeholder = "Ask something..." }) {
870
883
  const { sendMessage, isLoading } = useChatContext();
@@ -892,8 +905,9 @@ function Composer({ className, placeholder = "Ask something..." }) {
892
905
  {
893
906
  type: "submit",
894
907
  disabled: isLoading || !input.trim(),
895
- className: "absolute right-2 top-1/2 -translate-y-1/2 p-2 text-[var(--bui-fg-secondary,#a1a1aa)] hover:text-[var(--bui-fg,#f4f4f5)] disabled:opacity-30 disabled:cursor-not-allowed transition-colors",
896
- children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("path", { d: "M14.5 1.5L7 9M14.5 1.5L10 14.5L7 9M14.5 1.5L1.5 6L7 9", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }) })
908
+ "aria-label": "Send",
909
+ className: "absolute right-2 top-1/2 -translate-y-1/2 flex h-8 w-8 items-center justify-center rounded-full bg-[var(--bui-primary,#18181b)] text-[var(--bui-user-fg,#f4f4f5)] transition-all enabled:hover:bg-[var(--bui-primary-hover,#27272a)] enabled:active:scale-95 disabled:bg-[var(--bui-bg-elevated,#27272a)] disabled:text-[var(--bui-fg-faint,#52525b)] disabled:cursor-not-allowed",
910
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_lucide_react2.Send, { size: 14, strokeWidth: 1.75 })
897
911
  }
898
912
  )
899
913
  ] });
@@ -367,6 +367,7 @@ function ChatProvider({ endpoint = "/api/chat", tools, toolStateStore: externalS
367
367
 
368
368
  // src/components/Thread.tsx
369
369
  import { useRef as useRef3, useEffect as useEffect4 } from "react";
370
+ import { ArrowRight } from "lucide-react";
370
371
 
371
372
  // src/components/Message.tsx
372
373
  import {
@@ -777,18 +778,29 @@ function Thread({ className, emptyMessage, suggestions }) {
777
778
  }
778
779
  }, [messages, isLoading]);
779
780
  return /* @__PURE__ */ jsx5("div", { ref: scrollRef, className: `${className || ""}`, children: /* @__PURE__ */ jsxs4("div", { className: "p-6 space-y-6", children: [
780
- messages.length === 0 && /* @__PURE__ */ jsx5("div", { className: "text-center py-16", children: /* @__PURE__ */ jsxs4("div", { className: "text-[var(--bui-fg-faint,#52525b)] text-sm", children: [
781
- /* @__PURE__ */ jsx5("p", { className: "text-[var(--bui-fg-secondary,#a1a1aa)] mb-4", children: emptyMessage || "Send a message to get started" }),
782
- suggestions && suggestions.length > 0 && /* @__PURE__ */ jsx5("div", { className: "flex flex-wrap justify-center gap-2 mt-4", children: suggestions.map((suggestion) => /* @__PURE__ */ jsx5(
781
+ messages.length === 0 && /* @__PURE__ */ jsxs4("div", { className: "py-12", children: [
782
+ /* @__PURE__ */ jsx5("p", { className: "text-center text-sm text-[var(--bui-fg-secondary,#a1a1aa)]", children: emptyMessage || "Send a message to get started" }),
783
+ suggestions && suggestions.length > 0 && /* @__PURE__ */ jsx5("div", { className: "mt-8 flex flex-col divide-y divide-[var(--bui-border,#27272a)] border-y border-[var(--bui-border,#27272a)]", children: suggestions.map((suggestion) => /* @__PURE__ */ jsxs4(
783
784
  "button",
784
785
  {
785
786
  onClick: () => sendMessage(suggestion),
786
- className: "px-3 py-1.5 text-xs text-[var(--bui-fg-secondary,#a1a1aa)] bg-[var(--bui-bg-elevated,#27272a)] border border-[var(--bui-border-strong,#3f3f46)] rounded-full hover:bg-[var(--bui-bg-hover,#3f3f46)] hover:text-[var(--bui-fg,#f4f4f5)] transition-colors",
787
- children: suggestion
787
+ className: "group flex items-center justify-between gap-4 px-5 py-4 text-left text-sm text-[var(--bui-fg,#f4f4f5)] transition-colors hover:bg-[var(--bui-bg-hover,#27272a)]",
788
+ children: [
789
+ /* @__PURE__ */ jsx5("span", { className: "flex-1", children: suggestion }),
790
+ /* @__PURE__ */ jsx5(
791
+ ArrowRight,
792
+ {
793
+ size: 14,
794
+ strokeWidth: 1.75,
795
+ "aria-hidden": "true",
796
+ className: "shrink-0 text-[var(--bui-fg-faint,#52525b)] transition-all group-hover:translate-x-0.5 group-hover:text-[var(--bui-fg-secondary,#a1a1aa)]"
797
+ }
798
+ )
799
+ ]
788
800
  },
789
801
  suggestion
790
802
  )) })
791
- ] }) }),
803
+ ] }),
792
804
  messages.map((message, i) => /* @__PURE__ */ jsx5(
793
805
  Message,
794
806
  {
@@ -811,6 +823,7 @@ function Thread({ className, emptyMessage, suggestions }) {
811
823
 
812
824
  // src/components/Composer.tsx
813
825
  import { useState as useState3 } from "react";
826
+ import { Send } from "lucide-react";
814
827
  import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
815
828
  function Composer({ className, placeholder = "Ask something..." }) {
816
829
  const { sendMessage, isLoading } = useChatContext();
@@ -838,8 +851,9 @@ function Composer({ className, placeholder = "Ask something..." }) {
838
851
  {
839
852
  type: "submit",
840
853
  disabled: isLoading || !input.trim(),
841
- className: "absolute right-2 top-1/2 -translate-y-1/2 p-2 text-[var(--bui-fg-secondary,#a1a1aa)] hover:text-[var(--bui-fg,#f4f4f5)] disabled:opacity-30 disabled:cursor-not-allowed transition-colors",
842
- children: /* @__PURE__ */ jsx6("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: /* @__PURE__ */ jsx6("path", { d: "M14.5 1.5L7 9M14.5 1.5L10 14.5L7 9M14.5 1.5L1.5 6L7 9", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }) })
854
+ "aria-label": "Send",
855
+ className: "absolute right-2 top-1/2 -translate-y-1/2 flex h-8 w-8 items-center justify-center rounded-full bg-[var(--bui-primary,#18181b)] text-[var(--bui-user-fg,#f4f4f5)] transition-all enabled:hover:bg-[var(--bui-primary-hover,#27272a)] enabled:active:scale-95 disabled:bg-[var(--bui-bg-elevated,#27272a)] disabled:text-[var(--bui-fg-faint,#52525b)] disabled:cursor-not-allowed",
856
+ children: /* @__PURE__ */ jsx6(Send, { size: 14, strokeWidth: 1.75 })
843
857
  }
844
858
  )
845
859
  ] });
@@ -1,30 +1,8 @@
1
- import { T as Tool } from '../tool-yZTixiN2.mjs';
1
+ import { T as Tool, g as ToolContext } from '../tool-yZTixiN2.mjs';
2
2
  import { J as JsonSchema } from '../schema-DdZf6066.mjs';
3
3
  import 'zod';
4
4
  import 'react';
5
5
 
6
- /**
7
- * OpenAPI 3.1 spec generator for Better UI tools.
8
- *
9
- * Converts a tool registry into a valid OpenAPI document.
10
- * Each tool becomes a POST endpoint at `/api/tools/{toolName}`.
11
- *
12
- * @example
13
- * ```typescript
14
- * import { generateOpenAPISpec } from '@lantos1618/better-ui/openapi';
15
- * import { tools } from './tools';
16
- *
17
- * const spec = generateOpenAPISpec({
18
- * title: 'My AI Tools API',
19
- * version: '1.0.0',
20
- * tools,
21
- * });
22
- *
23
- * // Serve as JSON
24
- * app.get('/openapi.json', (req, res) => res.json(spec));
25
- * ```
26
- */
27
-
28
6
  interface OpenAPISpecConfig {
29
7
  /** API title */
30
8
  title: string;
@@ -109,5 +87,36 @@ declare function generateOpenAPISpec(config: OpenAPISpecConfig): OpenAPISpec;
109
87
  * ```
110
88
  */
111
89
  declare function openAPIHandler(config: OpenAPISpecConfig): (req: Request) => Response;
90
+ interface ToolRouterConfig {
91
+ /** Tool registry */
92
+ tools: Record<string, Tool>;
93
+ /** Base path prefix (default: '/api/tools') */
94
+ basePath?: string;
95
+ /** Optional context passed to every tool execution */
96
+ context?: Partial<ToolContext>;
97
+ /** Called before tool execution — throw to reject */
98
+ onBeforeExecute?: (toolName: string, input: unknown, req: Request) => Promise<void> | void;
99
+ }
100
+ /**
101
+ * Create a request handler that routes to individual tool endpoints.
102
+ * Each tool is callable at `POST {basePath}/{toolName}`.
103
+ *
104
+ * Also serves the OpenAPI spec at `GET {basePath}` and a Swagger UI at `GET {basePath}/docs`.
105
+ *
106
+ * @example
107
+ * ```typescript
108
+ * // Next.js catch-all: app/api/tools/[...path]/route.ts
109
+ * import { toolRouter } from '@lantos1618/better-ui/openapi';
110
+ * const router = toolRouter({ tools });
111
+ * export const GET = router;
112
+ * export const POST = router;
113
+ *
114
+ * // Now callable:
115
+ * // POST /api/tools/weather { "city": "Tokyo" } -> { "result": { "temp": 22, ... } }
116
+ * // GET /api/tools -> OpenAPI JSON spec
117
+ * // GET /api/tools/docs -> Swagger UI
118
+ * ```
119
+ */
120
+ declare function toolRouter(config: ToolRouterConfig): (req: Request) => Promise<Response>;
112
121
 
113
- export { type OpenAPISpec, type OpenAPISpecConfig, generateOpenAPISpec, openAPIHandler };
122
+ export { type OpenAPISpec, type OpenAPISpecConfig, type ToolRouterConfig, generateOpenAPISpec, openAPIHandler, toolRouter };
@@ -1,30 +1,8 @@
1
- import { T as Tool } from '../tool-yZTixiN2.js';
1
+ import { T as Tool, g as ToolContext } from '../tool-yZTixiN2.js';
2
2
  import { J as JsonSchema } from '../schema-DdZf6066.js';
3
3
  import 'zod';
4
4
  import 'react';
5
5
 
6
- /**
7
- * OpenAPI 3.1 spec generator for Better UI tools.
8
- *
9
- * Converts a tool registry into a valid OpenAPI document.
10
- * Each tool becomes a POST endpoint at `/api/tools/{toolName}`.
11
- *
12
- * @example
13
- * ```typescript
14
- * import { generateOpenAPISpec } from '@lantos1618/better-ui/openapi';
15
- * import { tools } from './tools';
16
- *
17
- * const spec = generateOpenAPISpec({
18
- * title: 'My AI Tools API',
19
- * version: '1.0.0',
20
- * tools,
21
- * });
22
- *
23
- * // Serve as JSON
24
- * app.get('/openapi.json', (req, res) => res.json(spec));
25
- * ```
26
- */
27
-
28
6
  interface OpenAPISpecConfig {
29
7
  /** API title */
30
8
  title: string;
@@ -109,5 +87,36 @@ declare function generateOpenAPISpec(config: OpenAPISpecConfig): OpenAPISpec;
109
87
  * ```
110
88
  */
111
89
  declare function openAPIHandler(config: OpenAPISpecConfig): (req: Request) => Response;
90
+ interface ToolRouterConfig {
91
+ /** Tool registry */
92
+ tools: Record<string, Tool>;
93
+ /** Base path prefix (default: '/api/tools') */
94
+ basePath?: string;
95
+ /** Optional context passed to every tool execution */
96
+ context?: Partial<ToolContext>;
97
+ /** Called before tool execution — throw to reject */
98
+ onBeforeExecute?: (toolName: string, input: unknown, req: Request) => Promise<void> | void;
99
+ }
100
+ /**
101
+ * Create a request handler that routes to individual tool endpoints.
102
+ * Each tool is callable at `POST {basePath}/{toolName}`.
103
+ *
104
+ * Also serves the OpenAPI spec at `GET {basePath}` and a Swagger UI at `GET {basePath}/docs`.
105
+ *
106
+ * @example
107
+ * ```typescript
108
+ * // Next.js catch-all: app/api/tools/[...path]/route.ts
109
+ * import { toolRouter } from '@lantos1618/better-ui/openapi';
110
+ * const router = toolRouter({ tools });
111
+ * export const GET = router;
112
+ * export const POST = router;
113
+ *
114
+ * // Now callable:
115
+ * // POST /api/tools/weather { "city": "Tokyo" } -> { "result": { "temp": 22, ... } }
116
+ * // GET /api/tools -> OpenAPI JSON spec
117
+ * // GET /api/tools/docs -> Swagger UI
118
+ * ```
119
+ */
120
+ declare function toolRouter(config: ToolRouterConfig): (req: Request) => Promise<Response>;
112
121
 
113
- export { type OpenAPISpec, type OpenAPISpecConfig, generateOpenAPISpec, openAPIHandler };
122
+ export { type OpenAPISpec, type OpenAPISpecConfig, type ToolRouterConfig, generateOpenAPISpec, openAPIHandler, toolRouter };
@@ -21,7 +21,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var openapi_exports = {};
22
22
  __export(openapi_exports, {
23
23
  generateOpenAPISpec: () => generateOpenAPISpec,
24
- openAPIHandler: () => openAPIHandler
24
+ openAPIHandler: () => openAPIHandler,
25
+ toolRouter: () => toolRouter
25
26
  });
26
27
  module.exports = __toCommonJS(openapi_exports);
27
28
 
@@ -253,8 +254,86 @@ function openAPIHandler(config) {
253
254
  }
254
255
  });
255
256
  }
257
+ function toolRouter(config) {
258
+ const basePath = config.basePath ?? "/api/tools";
259
+ return async (req) => {
260
+ const url = new URL(req.url);
261
+ const path = url.pathname;
262
+ if (req.method === "OPTIONS") {
263
+ return new Response(null, {
264
+ headers: {
265
+ "Access-Control-Allow-Origin": "*",
266
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
267
+ "Access-Control-Allow-Headers": "Content-Type, Authorization"
268
+ }
269
+ });
270
+ }
271
+ if (req.method === "GET" && (path === basePath || path === basePath + "/")) {
272
+ const spec = generateOpenAPISpec({
273
+ title: "Tool API",
274
+ version: "1.0.0",
275
+ tools: config.tools,
276
+ basePath,
277
+ serverUrl: url.origin
278
+ });
279
+ return new Response(JSON.stringify(spec, null, 2), {
280
+ headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" }
281
+ });
282
+ }
283
+ if (req.method === "GET" && (path === basePath + "/docs" || path === basePath + "/docs/")) {
284
+ const html = `<!DOCTYPE html><html><head><meta charset="utf-8"><title>API Docs</title>
285
+ <link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css">
286
+ </head><body><div id="swagger-ui"></div>
287
+ <script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
288
+ <script>SwaggerUIBundle({url:'${basePath}',dom_id:'#swagger-ui',deepLinking:true});</script>
289
+ </body></html>`;
290
+ return new Response(html, {
291
+ headers: { "Content-Type": "text/html", "Access-Control-Allow-Origin": "*" }
292
+ });
293
+ }
294
+ if (req.method === "POST" && path.startsWith(basePath + "/")) {
295
+ const toolName = path.slice(basePath.length + 1).replace(/\/$/, "");
296
+ if (!toolName) {
297
+ return Response.json({ error: "Missing tool name" }, { status: 400 });
298
+ }
299
+ if (!Object.prototype.hasOwnProperty.call(config.tools, toolName)) {
300
+ return Response.json({ error: "Tool not found" }, { status: 404 });
301
+ }
302
+ const tool = config.tools[toolName];
303
+ let input;
304
+ try {
305
+ input = await req.json();
306
+ } catch {
307
+ return Response.json({ error: "Invalid JSON body" }, { status: 400 });
308
+ }
309
+ try {
310
+ if (config.onBeforeExecute) {
311
+ await config.onBeforeExecute(toolName, input, req);
312
+ }
313
+ const result = await tool.run(input, {
314
+ isServer: true,
315
+ headers: req.headers,
316
+ ...config.context
317
+ });
318
+ return Response.json({ result }, {
319
+ headers: { "Access-Control-Allow-Origin": "*" }
320
+ });
321
+ } catch (error) {
322
+ if (error instanceof Error && error.name === "ZodError") {
323
+ return Response.json({ error: error.message }, { status: 400 });
324
+ }
325
+ return Response.json(
326
+ { error: error instanceof Error ? error.message : "Tool execution failed" },
327
+ { status: 500 }
328
+ );
329
+ }
330
+ }
331
+ return Response.json({ error: "Not found" }, { status: 404 });
332
+ };
333
+ }
256
334
  // Annotate the CommonJS export names for ESM import in node:
257
335
  0 && (module.exports = {
258
336
  generateOpenAPISpec,
259
- openAPIHandler
337
+ openAPIHandler,
338
+ toolRouter
260
339
  });
@@ -91,7 +91,85 @@ function openAPIHandler(config) {
91
91
  }
92
92
  });
93
93
  }
94
+ function toolRouter(config) {
95
+ const basePath = config.basePath ?? "/api/tools";
96
+ return async (req) => {
97
+ const url = new URL(req.url);
98
+ const path = url.pathname;
99
+ if (req.method === "OPTIONS") {
100
+ return new Response(null, {
101
+ headers: {
102
+ "Access-Control-Allow-Origin": "*",
103
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
104
+ "Access-Control-Allow-Headers": "Content-Type, Authorization"
105
+ }
106
+ });
107
+ }
108
+ if (req.method === "GET" && (path === basePath || path === basePath + "/")) {
109
+ const spec = generateOpenAPISpec({
110
+ title: "Tool API",
111
+ version: "1.0.0",
112
+ tools: config.tools,
113
+ basePath,
114
+ serverUrl: url.origin
115
+ });
116
+ return new Response(JSON.stringify(spec, null, 2), {
117
+ headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" }
118
+ });
119
+ }
120
+ if (req.method === "GET" && (path === basePath + "/docs" || path === basePath + "/docs/")) {
121
+ const html = `<!DOCTYPE html><html><head><meta charset="utf-8"><title>API Docs</title>
122
+ <link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css">
123
+ </head><body><div id="swagger-ui"></div>
124
+ <script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
125
+ <script>SwaggerUIBundle({url:'${basePath}',dom_id:'#swagger-ui',deepLinking:true});</script>
126
+ </body></html>`;
127
+ return new Response(html, {
128
+ headers: { "Content-Type": "text/html", "Access-Control-Allow-Origin": "*" }
129
+ });
130
+ }
131
+ if (req.method === "POST" && path.startsWith(basePath + "/")) {
132
+ const toolName = path.slice(basePath.length + 1).replace(/\/$/, "");
133
+ if (!toolName) {
134
+ return Response.json({ error: "Missing tool name" }, { status: 400 });
135
+ }
136
+ if (!Object.prototype.hasOwnProperty.call(config.tools, toolName)) {
137
+ return Response.json({ error: "Tool not found" }, { status: 404 });
138
+ }
139
+ const tool = config.tools[toolName];
140
+ let input;
141
+ try {
142
+ input = await req.json();
143
+ } catch {
144
+ return Response.json({ error: "Invalid JSON body" }, { status: 400 });
145
+ }
146
+ try {
147
+ if (config.onBeforeExecute) {
148
+ await config.onBeforeExecute(toolName, input, req);
149
+ }
150
+ const result = await tool.run(input, {
151
+ isServer: true,
152
+ headers: req.headers,
153
+ ...config.context
154
+ });
155
+ return Response.json({ result }, {
156
+ headers: { "Access-Control-Allow-Origin": "*" }
157
+ });
158
+ } catch (error) {
159
+ if (error instanceof Error && error.name === "ZodError") {
160
+ return Response.json({ error: error.message }, { status: 400 });
161
+ }
162
+ return Response.json(
163
+ { error: error instanceof Error ? error.message : "Tool execution failed" },
164
+ { status: 500 }
165
+ );
166
+ }
167
+ }
168
+ return Response.json({ error: "Not found" }, { status: 404 });
169
+ };
170
+ }
94
171
  export {
95
172
  generateOpenAPISpec,
96
- openAPIHandler
173
+ openAPIHandler,
174
+ toolRouter
97
175
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lantos1618/better-ui",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "description": "A minimal, type-safe AI-first UI framework for building tools",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -92,6 +92,7 @@
92
92
  "@ai-sdk/anthropic": "^3.0.0",
93
93
  "@ai-sdk/google": "^3.0.0",
94
94
  "jose": "^6.0.0",
95
+ "lucide-react": "^1.9.0",
95
96
  "react": "^19.0.0",
96
97
  "react-dom": "^19.0.0"
97
98
  },
@@ -123,6 +124,7 @@
123
124
  "jest": "^29.7.0",
124
125
  "jest-environment-jsdom": "^30.3.0",
125
126
  "jose": "^6.2.2",
127
+ "lucide-react": "^1.9.0",
126
128
  "react": "^19.2.4",
127
129
  "react-dom": "^19.2.4",
128
130
  "ts-jest": "^29.4.9",