@lantos1618/better-ui 0.2.2 → 0.3.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.
Files changed (59) hide show
  1. package/README.md +231 -148
  2. package/dist/index.d.mts +314 -0
  3. package/dist/index.d.ts +314 -0
  4. package/dist/index.js +522 -0
  5. package/dist/index.mjs +491 -0
  6. package/package.json +59 -20
  7. package/lib/aui/README.md +0 -136
  8. package/lib/aui/__tests__/aui-complete.test.ts +0 -251
  9. package/lib/aui/__tests__/aui-comprehensive.test.ts +0 -376
  10. package/lib/aui/__tests__/aui-concise.test.ts +0 -278
  11. package/lib/aui/__tests__/aui-integration.test.ts +0 -309
  12. package/lib/aui/__tests__/aui-simple.test.ts +0 -116
  13. package/lib/aui/__tests__/aui.test.ts +0 -269
  14. package/lib/aui/__tests__/concise-api.test.ts +0 -165
  15. package/lib/aui/__tests__/core.test.ts +0 -265
  16. package/lib/aui/__tests__/simple-api.test.ts +0 -200
  17. package/lib/aui/ai-assistant.ts +0 -408
  18. package/lib/aui/ai-control.ts +0 -353
  19. package/lib/aui/client/use-aui.ts +0 -55
  20. package/lib/aui/client-control.ts +0 -551
  21. package/lib/aui/client-executor.ts +0 -417
  22. package/lib/aui/components/ToolRenderer.tsx +0 -22
  23. package/lib/aui/core.ts +0 -137
  24. package/lib/aui/demo.tsx +0 -89
  25. package/lib/aui/examples/ai-complete-demo.tsx +0 -359
  26. package/lib/aui/examples/ai-control-demo.tsx +0 -356
  27. package/lib/aui/examples/ai-control-tools.ts +0 -308
  28. package/lib/aui/examples/concise-api.tsx +0 -153
  29. package/lib/aui/examples/index.tsx +0 -163
  30. package/lib/aui/examples/quick-demo.tsx +0 -91
  31. package/lib/aui/examples/simple-demo.tsx +0 -71
  32. package/lib/aui/examples/simple-tools.tsx +0 -160
  33. package/lib/aui/examples/user-api.tsx +0 -208
  34. package/lib/aui/examples/user-requested.tsx +0 -174
  35. package/lib/aui/examples/weather-search-tools.tsx +0 -119
  36. package/lib/aui/examples.tsx +0 -367
  37. package/lib/aui/hooks/useAUITool.ts +0 -142
  38. package/lib/aui/hooks/useAUIToolEnhanced.ts +0 -343
  39. package/lib/aui/hooks/useAUITools.ts +0 -195
  40. package/lib/aui/index.ts +0 -156
  41. package/lib/aui/provider.tsx +0 -45
  42. package/lib/aui/server-control.ts +0 -386
  43. package/lib/aui/server-executor.ts +0 -165
  44. package/lib/aui/server.ts +0 -167
  45. package/lib/aui/tool-registry.ts +0 -380
  46. package/lib/aui/tools/advanced-examples.tsx +0 -86
  47. package/lib/aui/tools/ai-complete.ts +0 -375
  48. package/lib/aui/tools/api-tools.tsx +0 -230
  49. package/lib/aui/tools/data-tools.tsx +0 -232
  50. package/lib/aui/tools/dom-tools.tsx +0 -202
  51. package/lib/aui/tools/examples.ts +0 -43
  52. package/lib/aui/tools/file-tools.tsx +0 -202
  53. package/lib/aui/tools/form-tools.tsx +0 -233
  54. package/lib/aui/tools/index.ts +0 -8
  55. package/lib/aui/tools/navigation-tools.tsx +0 -172
  56. package/lib/aui/tools/notification-tools.ts +0 -213
  57. package/lib/aui/tools/state-tools.tsx +0 -209
  58. package/lib/aui/types.ts +0 -47
  59. package/lib/aui/vercel-ai.ts +0 -100
package/dist/index.mjs ADDED
@@ -0,0 +1,491 @@
1
+ // src/tool.tsx
2
+ import { memo } from "react";
3
+ import { jsx } from "react/jsx-runtime";
4
+ var Tool = class {
5
+ constructor(config) {
6
+ this.name = config.name;
7
+ this.description = config.description;
8
+ this.inputSchema = config.input;
9
+ this.outputSchema = config.output;
10
+ this.tags = config.tags || [];
11
+ this.cacheConfig = config.cache;
12
+ this.clientFetchConfig = config.clientFetch;
13
+ this._initView();
14
+ }
15
+ /**
16
+ * Define server-side implementation
17
+ * Runs on server (API routes, server components, etc.)
18
+ */
19
+ server(handler) {
20
+ this._server = handler;
21
+ return this;
22
+ }
23
+ /**
24
+ * Define client-side implementation
25
+ * Runs in browser. If not specified, auto-fetches to /api/tools/{name}
26
+ */
27
+ client(handler) {
28
+ this._client = handler;
29
+ return this;
30
+ }
31
+ /**
32
+ * Define view component for rendering results
33
+ * Our differentiator from TanStack AI
34
+ */
35
+ view(component) {
36
+ this._view = component;
37
+ this._initView();
38
+ return this;
39
+ }
40
+ /**
41
+ * Execute the tool
42
+ * Automatically uses server or client handler based on environment
43
+ *
44
+ * SECURITY: Server handlers only run on server. Client automatically
45
+ * fetches from /api/tools/execute if no client handler is defined.
46
+ */
47
+ async run(input, ctx) {
48
+ const validated = this.inputSchema.parse(input);
49
+ const isServer = ctx?.isServer ?? typeof window === "undefined";
50
+ const context = {
51
+ cache: ctx?.cache || /* @__PURE__ */ new Map(),
52
+ fetch: ctx?.fetch || globalThis.fetch?.bind(globalThis),
53
+ isServer,
54
+ // Only include server-sensitive fields when actually on server
55
+ ...isServer ? {
56
+ env: ctx?.env,
57
+ headers: ctx?.headers,
58
+ cookies: ctx?.cookies,
59
+ user: ctx?.user,
60
+ session: ctx?.session
61
+ } : {
62
+ // Client-only fields
63
+ optimistic: ctx?.optimistic
64
+ }
65
+ };
66
+ if (this.cacheConfig && context.cache) {
67
+ const cacheKey = this.cacheConfig.key ? this.cacheConfig.key(validated) : `${this.name}:${JSON.stringify(validated)}`;
68
+ const cached = context.cache.get(cacheKey);
69
+ if (cached && cached.expiry > Date.now()) {
70
+ return cached.data;
71
+ }
72
+ }
73
+ let result;
74
+ if (context.isServer) {
75
+ if (!this._server) {
76
+ throw new Error(`Tool "${this.name}" has no server implementation`);
77
+ }
78
+ result = await this._server(validated, context);
79
+ } else {
80
+ if (this._client) {
81
+ result = await this._client(validated, context);
82
+ } else if (this._server) {
83
+ result = await this._defaultClientFetch(validated, context);
84
+ } else {
85
+ throw new Error(`Tool "${this.name}" has no implementation`);
86
+ }
87
+ }
88
+ if (this.outputSchema) {
89
+ try {
90
+ result = this.outputSchema.parse(result);
91
+ } catch (error) {
92
+ console.error(`Output validation failed for tool "${this.name}":`, error);
93
+ if (error instanceof Error && "errors" in error) {
94
+ console.error("Validation errors:", error.errors);
95
+ }
96
+ throw error;
97
+ }
98
+ }
99
+ if (this.cacheConfig && context.cache) {
100
+ const cacheKey = this.cacheConfig.key ? this.cacheConfig.key(validated) : `${this.name}:${JSON.stringify(validated)}`;
101
+ context.cache.set(cacheKey, {
102
+ data: result,
103
+ expiry: Date.now() + this.cacheConfig.ttl
104
+ });
105
+ }
106
+ return result;
107
+ }
108
+ /**
109
+ * Make the tool callable directly: await weather({ city: 'London' })
110
+ */
111
+ async call(input, ctx) {
112
+ return this.run(input, ctx);
113
+ }
114
+ /**
115
+ * Default client fetch when no .client() is defined
116
+ *
117
+ * SECURITY: This ensures server handlers never run on the client.
118
+ * The server-side /api/tools/execute endpoint handles execution safely.
119
+ */
120
+ async _defaultClientFetch(input, ctx) {
121
+ const endpoint = this.clientFetchConfig?.endpoint || "/api/tools/execute";
122
+ const response = await ctx.fetch(endpoint, {
123
+ method: "POST",
124
+ headers: { "Content-Type": "application/json" },
125
+ body: JSON.stringify({ tool: this.name, input })
126
+ });
127
+ if (!response.ok) {
128
+ const error = await response.json().catch(() => ({}));
129
+ throw new Error(error.message || error.error || `Tool execution failed: ${response.statusText}`);
130
+ }
131
+ const data = await response.json();
132
+ return data.result ?? data.data ?? data;
133
+ }
134
+ _initView() {
135
+ const viewFn = this._view;
136
+ const name = this.name;
137
+ const ViewComponent = (props) => {
138
+ if (!viewFn) {
139
+ if (props.loading) return null;
140
+ if (props.error) return null;
141
+ if (!props.data) return null;
142
+ return /* @__PURE__ */ jsx("pre", { children: JSON.stringify(props.data, null, 2) });
143
+ }
144
+ if (!props.data && !props.loading && !props.error) {
145
+ return null;
146
+ }
147
+ return viewFn(props.data, {
148
+ loading: props.loading,
149
+ error: props.error,
150
+ onAction: props.onAction
151
+ });
152
+ };
153
+ ViewComponent.displayName = `${name}View`;
154
+ this.View = memo(ViewComponent, (prevProps, nextProps) => {
155
+ if (prevProps.data !== nextProps.data) return false;
156
+ if (prevProps.loading !== nextProps.loading) return false;
157
+ if (prevProps.error !== nextProps.error) return false;
158
+ return true;
159
+ });
160
+ }
161
+ /**
162
+ * Check if tool has a view
163
+ */
164
+ get hasView() {
165
+ return !!this._view;
166
+ }
167
+ /**
168
+ * Check if tool has server implementation
169
+ */
170
+ get hasServer() {
171
+ return !!this._server;
172
+ }
173
+ /**
174
+ * Check if tool has custom client implementation
175
+ */
176
+ get hasClient() {
177
+ return !!this._client;
178
+ }
179
+ /**
180
+ * Convert to plain object (for serialization)
181
+ *
182
+ * SECURITY: This intentionally excludes handlers and schemas to prevent
183
+ * accidental exposure of server logic or validation details.
184
+ */
185
+ toJSON() {
186
+ return {
187
+ name: this.name,
188
+ description: this.description,
189
+ tags: this.tags,
190
+ hasServer: this.hasServer,
191
+ hasClient: this.hasClient,
192
+ hasView: this.hasView,
193
+ hasCache: !!this.cacheConfig
194
+ // Intentionally NOT included: handlers, schemas, clientFetchConfig
195
+ };
196
+ }
197
+ /**
198
+ * Convert to AI SDK format (Vercel AI SDK v5 compatible)
199
+ */
200
+ toAITool() {
201
+ return {
202
+ description: this.description || this.name,
203
+ inputSchema: this.inputSchema,
204
+ execute: async (input) => {
205
+ return this.run(input, { isServer: true });
206
+ }
207
+ };
208
+ }
209
+ };
210
+ function tool(nameOrConfig) {
211
+ if (typeof nameOrConfig === "string") {
212
+ return new ToolBuilder(nameOrConfig);
213
+ }
214
+ return new Tool(nameOrConfig);
215
+ }
216
+ var ToolBuilder = class {
217
+ constructor(name) {
218
+ this._tags = [];
219
+ this._name = name;
220
+ }
221
+ description(desc) {
222
+ this._description = desc;
223
+ return this;
224
+ }
225
+ /**
226
+ * Define input schema - enables type inference for handlers
227
+ *
228
+ * NOTE: Uses type assertion internally. This is safe because:
229
+ * 1. The schema is stored and used correctly at runtime
230
+ * 2. The return type correctly reflects the new generic parameter
231
+ * 3. TypeScript doesn't support "this type mutation" in fluent builders
232
+ */
233
+ input(schema) {
234
+ this._input = schema;
235
+ return this;
236
+ }
237
+ /**
238
+ * Define output schema - enables type inference for results
239
+ */
240
+ output(schema) {
241
+ this._output = schema;
242
+ return this;
243
+ }
244
+ tags(...tags) {
245
+ this._tags.push(...tags);
246
+ return this;
247
+ }
248
+ cache(config) {
249
+ this._cache = config;
250
+ return this;
251
+ }
252
+ /** Configure auto-fetch endpoint for client-side execution */
253
+ clientFetch(config) {
254
+ this._clientFetch = config;
255
+ return this;
256
+ }
257
+ server(handler) {
258
+ this._serverHandler = handler;
259
+ return this;
260
+ }
261
+ client(handler) {
262
+ this._clientHandler = handler;
263
+ return this;
264
+ }
265
+ view(component) {
266
+ this._viewComponent = component;
267
+ return this;
268
+ }
269
+ /**
270
+ * Build the final Tool instance
271
+ */
272
+ build() {
273
+ if (!this._input) {
274
+ throw new Error(`Tool "${this._name}" requires an input schema`);
275
+ }
276
+ const t = new Tool({
277
+ name: this._name,
278
+ description: this._description,
279
+ input: this._input,
280
+ output: this._output,
281
+ tags: this._tags,
282
+ cache: this._cache,
283
+ clientFetch: this._clientFetch
284
+ });
285
+ if (this._serverHandler) t.server(this._serverHandler);
286
+ if (this._clientHandler) t.client(this._clientHandler);
287
+ if (this._viewComponent) t.view(this._viewComponent);
288
+ return t;
289
+ }
290
+ /**
291
+ * Auto-build when accessing Tool methods
292
+ */
293
+ async run(input, ctx) {
294
+ return this.build().run(input, ctx);
295
+ }
296
+ get View() {
297
+ return this.build().View;
298
+ }
299
+ toJSON() {
300
+ return this.build().toJSON();
301
+ }
302
+ toAITool() {
303
+ return this.build().toAITool();
304
+ }
305
+ };
306
+
307
+ // src/react/useTool.ts
308
+ import { useState, useCallback, useEffect, useRef } from "react";
309
+ function useTool(tool2, initialInput, options = {}) {
310
+ const [data, setData] = useState(null);
311
+ const [loading, setLoading] = useState(false);
312
+ const [error, setError] = useState(null);
313
+ const [executed, setExecuted] = useState(false);
314
+ const inputRef = useRef(initialInput);
315
+ const optionsRef = useRef(options);
316
+ optionsRef.current = options;
317
+ const executionIdRef = useRef(0);
318
+ const pendingCountRef = useRef(0);
319
+ const execute = useCallback(
320
+ async (input) => {
321
+ const finalInput = input ?? inputRef.current;
322
+ if (finalInput === void 0) {
323
+ const err = new Error("No input provided to tool");
324
+ setError(err);
325
+ optionsRef.current.onError?.(err);
326
+ return null;
327
+ }
328
+ const currentExecutionId = ++executionIdRef.current;
329
+ pendingCountRef.current++;
330
+ setLoading(true);
331
+ setError(null);
332
+ try {
333
+ const context = {
334
+ cache: /* @__PURE__ */ new Map(),
335
+ fetch: globalThis.fetch?.bind(globalThis),
336
+ isServer: false,
337
+ ...optionsRef.current.context
338
+ };
339
+ const result = await tool2.run(finalInput, context);
340
+ if (currentExecutionId === executionIdRef.current) {
341
+ setData(result);
342
+ setExecuted(true);
343
+ optionsRef.current.onSuccess?.(result);
344
+ }
345
+ return result;
346
+ } catch (err) {
347
+ const error2 = err instanceof Error ? err : new Error(String(err));
348
+ if (currentExecutionId === executionIdRef.current) {
349
+ setError(error2);
350
+ optionsRef.current.onError?.(error2);
351
+ }
352
+ return null;
353
+ } finally {
354
+ pendingCountRef.current--;
355
+ if (pendingCountRef.current === 0) {
356
+ setLoading(false);
357
+ }
358
+ }
359
+ },
360
+ [tool2]
361
+ );
362
+ const reset = useCallback(() => {
363
+ setData(null);
364
+ setError(null);
365
+ setLoading(false);
366
+ setExecuted(false);
367
+ }, []);
368
+ useEffect(() => {
369
+ if (options.auto && initialInput !== void 0) {
370
+ inputRef.current = initialInput;
371
+ execute(initialInput);
372
+ }
373
+ }, [options.auto, initialInput, execute]);
374
+ return {
375
+ data,
376
+ loading,
377
+ error,
378
+ execute,
379
+ reset,
380
+ executed
381
+ };
382
+ }
383
+ function useTools(tools, options = {}) {
384
+ const toolsRef = useRef(tools);
385
+ const optionsRef = useRef(options);
386
+ optionsRef.current = options;
387
+ if (process.env.NODE_ENV !== "production") {
388
+ const prevKeys = Object.keys(toolsRef.current);
389
+ const currKeys = Object.keys(tools);
390
+ if (prevKeys.length !== currKeys.length || !currKeys.every((k) => prevKeys.includes(k))) {
391
+ console.warn(
392
+ "useTools: The tools object keys changed between renders. This may cause unexpected behavior. Define tools outside the component or memoize with useMemo."
393
+ );
394
+ }
395
+ toolsRef.current = tools;
396
+ }
397
+ const [state, setState] = useState(() => {
398
+ const initial = {};
399
+ for (const name of Object.keys(tools)) {
400
+ initial[name] = {
401
+ data: null,
402
+ loading: false,
403
+ error: null,
404
+ executed: false
405
+ };
406
+ }
407
+ return initial;
408
+ });
409
+ const createExecute = useCallback(
410
+ (toolName, tool2) => {
411
+ return async (input) => {
412
+ if (input === void 0) {
413
+ const err = new Error("No input provided to tool");
414
+ setState((prev) => ({
415
+ ...prev,
416
+ [toolName]: { ...prev[toolName], error: err }
417
+ }));
418
+ optionsRef.current.onError?.(err);
419
+ return null;
420
+ }
421
+ setState((prev) => ({
422
+ ...prev,
423
+ [toolName]: { ...prev[toolName], loading: true, error: null }
424
+ }));
425
+ try {
426
+ const context = {
427
+ cache: /* @__PURE__ */ new Map(),
428
+ fetch: globalThis.fetch?.bind(globalThis),
429
+ isServer: false,
430
+ ...optionsRef.current.context
431
+ };
432
+ const result = await tool2.run(input, context);
433
+ setState((prev) => ({
434
+ ...prev,
435
+ [toolName]: {
436
+ data: result,
437
+ loading: false,
438
+ error: null,
439
+ executed: true
440
+ }
441
+ }));
442
+ optionsRef.current.onSuccess?.(result);
443
+ return result;
444
+ } catch (err) {
445
+ const error = err instanceof Error ? err : new Error(String(err));
446
+ setState((prev) => ({
447
+ ...prev,
448
+ [toolName]: { ...prev[toolName], loading: false, error }
449
+ }));
450
+ optionsRef.current.onError?.(error);
451
+ return null;
452
+ }
453
+ };
454
+ },
455
+ []
456
+ );
457
+ const createReset = useCallback((toolName) => {
458
+ return () => {
459
+ setState((prev) => ({
460
+ ...prev,
461
+ [toolName]: {
462
+ data: null,
463
+ loading: false,
464
+ error: null,
465
+ executed: false
466
+ }
467
+ }));
468
+ };
469
+ }, []);
470
+ const results = {};
471
+ for (const [name, tool2] of Object.entries(tools)) {
472
+ const toolName = name;
473
+ const toolState = state[toolName];
474
+ results[toolName] = {
475
+ data: toolState?.data ?? null,
476
+ loading: toolState?.loading ?? false,
477
+ error: toolState?.error ?? null,
478
+ executed: toolState?.executed ?? false,
479
+ execute: createExecute(toolName, tool2),
480
+ reset: createReset(toolName)
481
+ };
482
+ }
483
+ return results;
484
+ }
485
+ export {
486
+ Tool,
487
+ ToolBuilder,
488
+ tool,
489
+ useTool,
490
+ useTools
491
+ };
package/package.json CHANGED
@@ -1,11 +1,19 @@
1
1
  {
2
2
  "name": "@lantos1618/better-ui",
3
- "version": "0.2.2",
4
- "description": "A modern UI framework for building AI-powered applications",
5
- "main": "lib/aui/index.ts",
6
- "types": "lib/aui/index.ts",
3
+ "version": "0.3.1",
4
+ "description": "A minimal, type-safe AI-first UI framework for building tools",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ }
14
+ },
7
15
  "files": [
8
- "lib/**/*",
16
+ "dist",
9
17
  "README.md",
10
18
  "LICENSE"
11
19
  ],
@@ -14,39 +22,70 @@
14
22
  "framework",
15
23
  "ai",
16
24
  "react",
17
- "nextjs"
25
+ "tools",
26
+ "typescript"
18
27
  ],
19
28
  "author": "Lyndon Leong",
20
29
  "license": "MIT",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "https://github.com/lantos1618/better-ui"
33
+ },
21
34
  "scripts": {
22
35
  "dev": "next dev",
23
- "build": "next build",
36
+ "build": "npm run build:lib && npm run build:next",
37
+ "build:lib": "tsup src/index.ts --format cjs,esm --dts --clean --tsconfig tsconfig.lib.json",
38
+ "build:next": "next build",
24
39
  "start": "next start",
25
40
  "lint": "eslint .",
26
41
  "test": "jest",
27
- "type-check": "tsc --noEmit"
42
+ "type-check": "tsc --noEmit",
43
+ "prepublishOnly": "npm run build:lib"
28
44
  },
29
45
  "dependencies": {
30
- "@ai-sdk/openai": "^2.0.20",
31
- "@sendgrid/mail": "^8.1.5",
32
- "ai": "^5.0.23",
33
- "magicpath-ai": "^1.1.2",
34
- "next": "^15.5.0",
35
- "react": "^18.3.0",
36
- "react-dom": "^18.3.0",
37
- "zod": "^3.22.0"
46
+ "@ai-sdk/openai": "^2.0.77",
47
+ "@ai-sdk/react": "^2.0.107",
48
+ "ai": "^5.0.107",
49
+ "zod": "^3.24.0"
50
+ },
51
+ "peerDependencies": {
52
+ "react": "^19.0.0",
53
+ "react-dom": "^19.0.0"
54
+ },
55
+ "peerDependenciesMeta": {
56
+ "react": {
57
+ "optional": true
58
+ },
59
+ "react-dom": {
60
+ "optional": true
61
+ }
38
62
  },
39
63
  "devDependencies": {
64
+ "@dnd-kit/core": "^6.3.1",
65
+ "@dnd-kit/sortable": "^10.0.0",
40
66
  "@eslint/eslintrc": "^3.3.1",
41
67
  "@jest/globals": "^29.7.0",
68
+ "@tailwindcss/postcss": "^4.1.17",
69
+ "@testing-library/jest-dom": "^6.9.1",
70
+ "@testing-library/react": "^16.3.0",
42
71
  "@types/jest": "^30.0.0",
43
72
  "@types/node": "^20",
44
- "@types/react": "^18",
45
- "@types/react-dom": "^18",
46
- "eslint": "^8",
47
- "eslint-config-next": "14.2.0",
73
+ "@types/react": "^19",
74
+ "@types/react-dom": "^19",
75
+ "autoprefixer": "^10.4.22",
76
+ "eslint": "^9",
77
+ "eslint-config-next": "^16.0.1",
78
+ "framer-motion": "^12.23.12",
48
79
  "jest": "^29.7.0",
80
+ "jest-environment-jsdom": "^30.2.0",
81
+ "lucide-react": "^0.543.0",
82
+ "next": "^16.0.1",
83
+ "postcss": "^8.5.6",
84
+ "react": "^19.2.1",
85
+ "react-dom": "^19.2.1",
86
+ "tailwindcss": "^4.1.17",
49
87
  "ts-jest": "^29.4.1",
88
+ "tsup": "^8.5.1",
50
89
  "typescript": "^5"
51
90
  }
52
91
  }