@lantos1618/better-ui 0.3.1 → 0.4.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/dist/index.mjs CHANGED
@@ -1,3 +1,7 @@
1
+ import {
2
+ __require
3
+ } from "./chunk-Y6FXYEAI.mjs";
4
+
1
5
  // src/tool.tsx
2
6
  import { memo } from "react";
3
7
  import { jsx } from "react/jsx-runtime";
@@ -10,6 +14,10 @@ var Tool = class {
10
14
  this.tags = config.tags || [];
11
15
  this.cacheConfig = config.cache;
12
16
  this.clientFetchConfig = config.clientFetch;
17
+ this.confirm = config.confirm ?? false;
18
+ this.hints = config.hints ?? {};
19
+ this.groupKey = config.groupKey;
20
+ this.autoRespond = config.autoRespond ?? false;
13
21
  this._initView();
14
22
  }
15
23
  /**
@@ -37,6 +45,14 @@ var Tool = class {
37
45
  this._initView();
38
46
  return this;
39
47
  }
48
+ /**
49
+ * Define streaming implementation
50
+ * The handler receives a `stream` callback to push partial updates.
51
+ */
52
+ stream(handler) {
53
+ this._stream = handler;
54
+ return this;
55
+ }
40
56
  /**
41
57
  * Execute the tool
42
58
  * Automatically uses server or client handler based on environment
@@ -111,6 +127,72 @@ var Tool = class {
111
127
  async call(input, ctx) {
112
128
  return this.run(input, ctx);
113
129
  }
130
+ /**
131
+ * Execute with streaming - returns async generator of partial results.
132
+ * Falls back to run() if no stream handler is defined.
133
+ */
134
+ async *runStream(input, ctx) {
135
+ const validated = this.inputSchema.parse(input);
136
+ if (!this._stream) {
137
+ const result = await this.run(input, ctx);
138
+ yield { partial: result, done: true };
139
+ return;
140
+ }
141
+ const isServer = ctx?.isServer ?? typeof window === "undefined";
142
+ const context = {
143
+ cache: ctx?.cache || /* @__PURE__ */ new Map(),
144
+ fetch: ctx?.fetch || globalThis.fetch?.bind(globalThis),
145
+ isServer,
146
+ ...isServer ? {
147
+ env: ctx?.env,
148
+ headers: ctx?.headers,
149
+ cookies: ctx?.cookies,
150
+ user: ctx?.user,
151
+ session: ctx?.session
152
+ } : {
153
+ optimistic: ctx?.optimistic
154
+ }
155
+ };
156
+ const queue = [];
157
+ let waiter = null;
158
+ const notify = () => {
159
+ waiter?.();
160
+ waiter = null;
161
+ };
162
+ const wait = () => new Promise((r) => {
163
+ waiter = r;
164
+ });
165
+ const streamFn = (partial) => {
166
+ queue.push({ partial });
167
+ notify();
168
+ };
169
+ let error = null;
170
+ let isDone = false;
171
+ this._stream(validated, { ...context, stream: streamFn }).then((result) => {
172
+ queue.push({ done: true, result });
173
+ isDone = true;
174
+ notify();
175
+ }).catch((err) => {
176
+ error = err instanceof Error ? err : new Error(String(err));
177
+ isDone = true;
178
+ notify();
179
+ });
180
+ while (true) {
181
+ while (queue.length > 0) {
182
+ const item = queue.shift();
183
+ if ("done" in item) {
184
+ let finalResult = item.result;
185
+ if (this.outputSchema) finalResult = this.outputSchema.parse(finalResult);
186
+ yield { partial: finalResult, done: true };
187
+ return;
188
+ }
189
+ yield { partial: item.partial, done: false };
190
+ }
191
+ if (error) throw error;
192
+ if (isDone && queue.length === 0) return;
193
+ await wait();
194
+ }
195
+ }
114
196
  /**
115
197
  * Default client fetch when no .client() is defined
116
198
  *
@@ -146,6 +228,7 @@ var Tool = class {
146
228
  }
147
229
  return viewFn(props.data, {
148
230
  loading: props.loading,
231
+ streaming: props.streaming,
149
232
  error: props.error,
150
233
  onAction: props.onAction
151
234
  });
@@ -154,6 +237,7 @@ var Tool = class {
154
237
  this.View = memo(ViewComponent, (prevProps, nextProps) => {
155
238
  if (prevProps.data !== nextProps.data) return false;
156
239
  if (prevProps.loading !== nextProps.loading) return false;
240
+ if (prevProps.streaming !== nextProps.streaming) return false;
157
241
  if (prevProps.error !== nextProps.error) return false;
158
242
  return true;
159
243
  });
@@ -176,6 +260,43 @@ var Tool = class {
176
260
  get hasClient() {
177
261
  return !!this._client;
178
262
  }
263
+ /**
264
+ * Check if tool has a streaming implementation
265
+ */
266
+ get hasStream() {
267
+ return !!this._stream;
268
+ }
269
+ /**
270
+ * Check if tool requires human confirmation before executing (HITL)
271
+ * Returns true if `confirm` is truthy (boolean true or a function) OR `hints.destructive: true`
272
+ */
273
+ get requiresConfirmation() {
274
+ return !!this.confirm || this.hints.destructive === true;
275
+ }
276
+ /**
277
+ * Determine if a specific input should trigger user confirmation.
278
+ * - If `confirm` is a function, calls it with input
279
+ * - If `confirm` is boolean, returns it
280
+ * - If `hints.destructive`, returns true
281
+ */
282
+ shouldConfirm(input) {
283
+ if (typeof this.confirm === "function") {
284
+ if (input == null) return false;
285
+ return this.confirm(input);
286
+ }
287
+ if (typeof this.confirm === "boolean") {
288
+ return this.confirm;
289
+ }
290
+ return this.hints.destructive === true;
291
+ }
292
+ /**
293
+ * Get the entity group key for a given input.
294
+ * Returns `"toolName:groupKey(input)"` if groupKey is defined, otherwise undefined.
295
+ */
296
+ getGroupKey(input) {
297
+ if (!this.groupKey || input == null) return void 0;
298
+ return `${this.name}:${this.groupKey(input)}`;
299
+ }
179
300
  /**
180
301
  * Convert to plain object (for serialization)
181
302
  *
@@ -190,14 +311,28 @@ var Tool = class {
190
311
  hasServer: this.hasServer,
191
312
  hasClient: this.hasClient,
192
313
  hasView: this.hasView,
193
- hasCache: !!this.cacheConfig
314
+ hasStream: this.hasStream,
315
+ hasCache: !!this.cacheConfig,
316
+ confirm: !!this.confirm,
317
+ hints: this.hints,
318
+ requiresConfirmation: this.requiresConfirmation
194
319
  // Intentionally NOT included: handlers, schemas, clientFetchConfig
195
320
  };
196
321
  }
197
322
  /**
198
323
  * Convert to AI SDK format (Vercel AI SDK v5 compatible)
324
+ *
325
+ * If `confirm` is true, the execute function is omitted so the AI SDK
326
+ * leaves the tool call at `state: 'input-available'`, enabling HITL
327
+ * confirmation on the client before execution.
199
328
  */
200
329
  toAITool() {
330
+ if (this.requiresConfirmation) {
331
+ return {
332
+ description: this.description || this.name,
333
+ inputSchema: this.inputSchema
334
+ };
335
+ }
201
336
  return {
202
337
  description: this.description || this.name,
203
338
  inputSchema: this.inputSchema,
@@ -254,6 +389,26 @@ var ToolBuilder = class {
254
389
  this._clientFetch = config;
255
390
  return this;
256
391
  }
392
+ /** Require human confirmation before executing (HITL) */
393
+ requireConfirm(value = true) {
394
+ this._confirm = value;
395
+ return this;
396
+ }
397
+ /** Set a groupKey function for collapsing related tool calls */
398
+ groupBy(fn) {
399
+ this._groupKey = fn;
400
+ return this;
401
+ }
402
+ /** Auto-send updated state to AI after user interacts with this tool's UI */
403
+ autoRespondAfterAction(value = true) {
404
+ this._autoRespond = value;
405
+ return this;
406
+ }
407
+ /** Set behavioral hints for the tool */
408
+ hints(hints) {
409
+ this._hints = hints;
410
+ return this;
411
+ }
257
412
  server(handler) {
258
413
  this._serverHandler = handler;
259
414
  return this;
@@ -262,6 +417,10 @@ var ToolBuilder = class {
262
417
  this._clientHandler = handler;
263
418
  return this;
264
419
  }
420
+ stream(handler) {
421
+ this._streamHandler = handler;
422
+ return this;
423
+ }
265
424
  view(component) {
266
425
  this._viewComponent = component;
267
426
  return this;
@@ -280,10 +439,15 @@ var ToolBuilder = class {
280
439
  output: this._output,
281
440
  tags: this._tags,
282
441
  cache: this._cache,
283
- clientFetch: this._clientFetch
442
+ clientFetch: this._clientFetch,
443
+ confirm: this._confirm,
444
+ hints: this._hints,
445
+ groupKey: this._groupKey,
446
+ autoRespond: this._autoRespond
284
447
  });
285
448
  if (this._serverHandler) t.server(this._serverHandler);
286
449
  if (this._clientHandler) t.client(this._clientHandler);
450
+ if (this._streamHandler) t.stream(this._streamHandler);
287
451
  if (this._viewComponent) t.view(this._viewComponent);
288
452
  return t;
289
453
  }
@@ -293,6 +457,9 @@ var ToolBuilder = class {
293
457
  async run(input, ctx) {
294
458
  return this.build().run(input, ctx);
295
459
  }
460
+ async *runStream(input, ctx) {
461
+ yield* this.build().runStream(input, ctx);
462
+ }
296
463
  get View() {
297
464
  return this.build().View;
298
465
  }
@@ -304,188 +471,87 @@ var ToolBuilder = class {
304
471
  }
305
472
  };
306
473
 
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);
474
+ // src/providers/openai.ts
475
+ function createOpenAIProvider(config) {
476
+ return {
477
+ type: "openai",
478
+ modelId: config.model,
479
+ model: () => {
480
+ const id = "@ai-sdk/openai";
481
+ const { openai } = __require(id);
482
+ return openai(config.model);
372
483
  }
373
- }, [options.auto, initialInput, execute]);
484
+ };
485
+ }
486
+
487
+ // src/providers/anthropic.ts
488
+ function createAnthropicProvider(config) {
374
489
  return {
375
- data,
376
- loading,
377
- error,
378
- execute,
379
- reset,
380
- executed
490
+ type: "anthropic",
491
+ modelId: config.model,
492
+ model: () => {
493
+ const id = "@ai-sdk/anthropic";
494
+ const { anthropic } = __require(id);
495
+ return anthropic(config.model);
496
+ }
381
497
  };
382
498
  }
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
- );
499
+
500
+ // src/providers/google.ts
501
+ function createGoogleProvider(config) {
502
+ return {
503
+ type: "google",
504
+ modelId: config.model,
505
+ model: () => {
506
+ const id = "@ai-sdk/google";
507
+ const { google } = __require(id);
508
+ return google(config.model);
394
509
  }
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
- };
510
+ };
511
+ }
512
+
513
+ // src/providers/openrouter.ts
514
+ function createOpenRouterProvider(config) {
515
+ return {
516
+ type: "openrouter",
517
+ modelId: config.model,
518
+ model: () => {
519
+ const id = "@ai-sdk/openai";
520
+ const { createOpenAI } = __require(id);
521
+ const openrouter = createOpenAI({
522
+ baseURL: config.baseURL || "https://openrouter.ai/api/v1",
523
+ apiKey: config.apiKey,
524
+ ...config.options
525
+ });
526
+ return openrouter(config.model);
527
+ }
528
+ };
529
+ }
530
+
531
+ // src/providers/index.ts
532
+ function createProvider(config) {
533
+ switch (config.provider) {
534
+ case "openai":
535
+ return createOpenAIProvider(config);
536
+ case "anthropic":
537
+ return createAnthropicProvider(config);
538
+ case "google":
539
+ return createGoogleProvider(config);
540
+ case "openrouter":
541
+ return createOpenRouterProvider(config);
542
+ default: {
543
+ const exhaustiveCheck = config.provider;
544
+ throw new Error(`Unknown provider: ${exhaustiveCheck}`);
406
545
  }
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
546
  }
483
- return results;
484
547
  }
485
548
  export {
486
549
  Tool,
487
550
  ToolBuilder,
488
- tool,
489
- useTool,
490
- useTools
551
+ createAnthropicProvider,
552
+ createGoogleProvider,
553
+ createOpenAIProvider,
554
+ createOpenRouterProvider,
555
+ createProvider,
556
+ tool
491
557
  };
@@ -0,0 +1,11 @@
1
+ import { P as PersistenceAdapter } from '../types-CAOfGUPH.mjs';
2
+ export { T as Thread } from '../types-CAOfGUPH.mjs';
3
+ import 'ai';
4
+
5
+ /**
6
+ * In-memory persistence adapter.
7
+ * Useful for development and testing. Data is lost on page refresh.
8
+ */
9
+ declare function createMemoryAdapter(): PersistenceAdapter;
10
+
11
+ export { PersistenceAdapter, createMemoryAdapter };
@@ -0,0 +1,11 @@
1
+ import { P as PersistenceAdapter } from '../types-CAOfGUPH.js';
2
+ export { T as Thread } from '../types-CAOfGUPH.js';
3
+ import 'ai';
4
+
5
+ /**
6
+ * In-memory persistence adapter.
7
+ * Useful for development and testing. Data is lost on page refresh.
8
+ */
9
+ declare function createMemoryAdapter(): PersistenceAdapter;
10
+
11
+ export { PersistenceAdapter, createMemoryAdapter };
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/persistence/index.ts
21
+ var persistence_exports = {};
22
+ __export(persistence_exports, {
23
+ createMemoryAdapter: () => createMemoryAdapter
24
+ });
25
+ module.exports = __toCommonJS(persistence_exports);
26
+
27
+ // src/persistence/memory.ts
28
+ function createMemoryAdapter() {
29
+ const threads = /* @__PURE__ */ new Map();
30
+ const messages = /* @__PURE__ */ new Map();
31
+ let counter = 0;
32
+ return {
33
+ async listThreads() {
34
+ return Array.from(threads.values()).sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime());
35
+ },
36
+ async getThread(id) {
37
+ return threads.get(id) ?? null;
38
+ },
39
+ async createThread(title) {
40
+ const id = `thread-${++counter}`;
41
+ const now = /* @__PURE__ */ new Date();
42
+ const thread = { id, title, createdAt: now, updatedAt: now };
43
+ threads.set(id, thread);
44
+ messages.set(id, []);
45
+ return thread;
46
+ },
47
+ async deleteThread(id) {
48
+ threads.delete(id);
49
+ messages.delete(id);
50
+ },
51
+ async getMessages(threadId) {
52
+ return messages.get(threadId) ?? [];
53
+ },
54
+ async saveMessages(threadId, msgs) {
55
+ messages.set(threadId, msgs);
56
+ const thread = threads.get(threadId);
57
+ if (thread) {
58
+ thread.updatedAt = /* @__PURE__ */ new Date();
59
+ }
60
+ }
61
+ };
62
+ }
63
+ // Annotate the CommonJS export names for ESM import in node:
64
+ 0 && (module.exports = {
65
+ createMemoryAdapter
66
+ });
@@ -0,0 +1,41 @@
1
+ import "../chunk-Y6FXYEAI.mjs";
2
+
3
+ // src/persistence/memory.ts
4
+ function createMemoryAdapter() {
5
+ const threads = /* @__PURE__ */ new Map();
6
+ const messages = /* @__PURE__ */ new Map();
7
+ let counter = 0;
8
+ return {
9
+ async listThreads() {
10
+ return Array.from(threads.values()).sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime());
11
+ },
12
+ async getThread(id) {
13
+ return threads.get(id) ?? null;
14
+ },
15
+ async createThread(title) {
16
+ const id = `thread-${++counter}`;
17
+ const now = /* @__PURE__ */ new Date();
18
+ const thread = { id, title, createdAt: now, updatedAt: now };
19
+ threads.set(id, thread);
20
+ messages.set(id, []);
21
+ return thread;
22
+ },
23
+ async deleteThread(id) {
24
+ threads.delete(id);
25
+ messages.delete(id);
26
+ },
27
+ async getMessages(threadId) {
28
+ return messages.get(threadId) ?? [];
29
+ },
30
+ async saveMessages(threadId, msgs) {
31
+ messages.set(threadId, msgs);
32
+ const thread = threads.get(threadId);
33
+ if (thread) {
34
+ thread.updatedAt = /* @__PURE__ */ new Date();
35
+ }
36
+ }
37
+ };
38
+ }
39
+ export {
40
+ createMemoryAdapter
41
+ };