@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/README.md CHANGED
@@ -10,7 +10,12 @@
10
10
 
11
11
  Better UI provides a clean, fluent API for creating tools that AI assistants can execute. Define input/output schemas with Zod, implement server and client logic separately, and render results with React components.
12
12
 
13
- **Key differentiator**: Unlike other AI tool libraries, Better UI includes **view integration** - tools can render their own results in UI.
13
+ **Key differentiators**:
14
+ - **View integration** - tools render their own results in UI (no other framework does this)
15
+ - **Multi-provider support** - OpenAI, Anthropic, Google Gemini, OpenRouter
16
+ - **Streaming tool views** - progressive partial data rendering
17
+ - **Pre-made chat components** - drop-in `<Chat />` with automatic tool view rendering
18
+ - **Server infrastructure** - rate limiting, caching, Next.js/Express adapters
14
19
 
15
20
  ## Installation
16
21
 
@@ -40,12 +45,127 @@ weather.server(async ({ city }) => {
40
45
  // View for rendering results (our differentiator!)
41
46
  weather.view((data) => (
42
47
  <div className="weather-card">
43
- <span>{data.temp}°</span>
48
+ <span>{data.temp}</span>
44
49
  <span>{data.condition}</span>
45
50
  </div>
46
51
  ));
47
52
  ```
48
53
 
54
+ ## Drop-in Chat UI
55
+
56
+ ```tsx
57
+ import { Chat } from '@lantos1618/better-ui/components';
58
+
59
+ function App() {
60
+ return (
61
+ <Chat
62
+ endpoint="/api/chat"
63
+ tools={{ weather, search, counter }}
64
+ className="h-[600px]"
65
+ placeholder="Ask something..."
66
+ />
67
+ );
68
+ }
69
+ ```
70
+
71
+ Or compose your own layout:
72
+
73
+ ```tsx
74
+ import { ChatProvider, Thread, Composer } from '@lantos1618/better-ui/components';
75
+
76
+ function App() {
77
+ return (
78
+ <ChatProvider endpoint="/api/chat" tools={tools}>
79
+ <div className="flex flex-col h-screen">
80
+ <Thread className="flex-1 overflow-y-auto" />
81
+ <Composer placeholder="Type a message..." />
82
+ </div>
83
+ </ChatProvider>
84
+ );
85
+ }
86
+ ```
87
+
88
+ Tool results in chat automatically render using the tool's `.view()` component.
89
+
90
+ ## Multi-Provider Support
91
+
92
+ ```typescript
93
+ import { createProvider } from '@lantos1618/better-ui';
94
+
95
+ // OpenAI
96
+ const provider = createProvider({ provider: 'openai', model: 'gpt-4o' });
97
+
98
+ // Anthropic
99
+ const provider = createProvider({ provider: 'anthropic', model: 'claude-4-sonnet' });
100
+
101
+ // Google Gemini
102
+ const provider = createProvider({ provider: 'google', model: 'gemini-2.5-pro' });
103
+
104
+ // OpenRouter (access any model)
105
+ const provider = createProvider({
106
+ provider: 'openrouter',
107
+ model: 'anthropic/claude-4-sonnet',
108
+ apiKey: process.env.OPENROUTER_API_KEY,
109
+ });
110
+ ```
111
+
112
+ Use with the chat handler:
113
+
114
+ ```typescript
115
+ import { streamText, convertToModelMessages } from 'ai';
116
+ import { createProvider } from '@lantos1618/better-ui';
117
+
118
+ const provider = createProvider({ provider: 'openai', model: 'gpt-4o' });
119
+
120
+ export async function POST(req: Request) {
121
+ const { messages } = await req.json();
122
+ const result = streamText({
123
+ model: provider.model(),
124
+ messages: convertToModelMessages(messages),
125
+ tools: {
126
+ weather: weatherTool.toAITool(),
127
+ },
128
+ });
129
+ return result.toUIMessageStreamResponse();
130
+ }
131
+ ```
132
+
133
+ ## Streaming Tool Views
134
+
135
+ Tools can stream partial results progressively:
136
+
137
+ ```typescript
138
+ import { useToolStream } from '@lantos1618/better-ui';
139
+
140
+ // Define a streaming tool
141
+ const analysis = tool({
142
+ name: 'analysis',
143
+ input: z.object({ query: z.string() }),
144
+ output: z.object({ summary: z.string(), score: z.number() }),
145
+ });
146
+
147
+ analysis.stream(async ({ query }, { stream }) => {
148
+ stream({ summary: 'Analyzing...' }); // Partial update
149
+ const result = await analyzeData(query);
150
+ stream({ summary: result.summary }); // More data
151
+ return { summary: result.summary, score: result.score }; // Final
152
+ });
153
+
154
+ // In a component:
155
+ function AnalysisWidget({ query }) {
156
+ const { data, streaming, loading, execute } = useToolStream(analysis);
157
+
158
+ return (
159
+ <div>
160
+ <button onClick={() => execute({ query })}>Analyze</button>
161
+ {loading && <p>Starting...</p>}
162
+ {streaming && <p>Streaming: {data?.summary}</p>}
163
+ {data?.score && <p>Score: {data.score}</p>}
164
+ </div>
165
+ );
166
+ }
167
+ ```
168
+
49
169
  ## Core Concepts
50
170
 
51
171
  ### 1. Tool Definition
@@ -79,7 +199,6 @@ The `.client()` method defines what happens when called from the browser. If not
79
199
 
80
200
  ```typescript
81
201
  myTool.client(async ({ query }, ctx) => {
82
- // Custom client logic: caching, optimistic updates, etc.
83
202
  const cached = ctx.cache.get(query);
84
203
  if (cached) return cached;
85
204
 
@@ -95,13 +214,27 @@ myTool.client(async ({ query }, ctx) => {
95
214
  The `.view()` method defines how to render the tool's results:
96
215
 
97
216
  ```typescript
98
- myTool.view((data, { loading, error }) => {
217
+ myTool.view((data, { loading, error, streaming }) => {
99
218
  if (loading) return <Spinner />;
100
219
  if (error) return <Error message={error.message} />;
220
+ if (streaming) return <PartialResults data={data} />;
101
221
  return <Results items={data.results} />;
102
222
  });
103
223
  ```
104
224
 
225
+ ### 5. Streaming
226
+
227
+ The `.stream()` method enables progressive partial updates:
228
+
229
+ ```typescript
230
+ myTool.stream(async ({ query }, { stream }) => {
231
+ stream({ status: 'searching...' });
232
+ const results = await search(query);
233
+ stream({ results, status: 'done' });
234
+ return { results, status: 'done', count: results.length };
235
+ });
236
+ ```
237
+
105
238
  ## Fluent Builder Alternative
106
239
 
107
240
  ```typescript
@@ -110,152 +243,119 @@ const search = tool('search')
110
243
  .input(z.object({ query: z.string() }))
111
244
  .output(z.object({ results: z.array(z.string()) }))
112
245
  .server(async ({ query }) => ({ results: await db.search(query) }))
246
+ .stream(async ({ query }, { stream }) => {
247
+ stream({ results: [] });
248
+ const results = await db.search(query);
249
+ return { results };
250
+ })
113
251
  .view((data) => <ResultsList items={data.results} />);
114
252
  ```
115
253
 
116
- ## React Usage
254
+ ## React Hooks
255
+
256
+ ### `useTool(tool, input?, options?)`
117
257
 
118
258
  ```typescript
119
259
  import { useTool } from '@lantos1618/better-ui';
120
260
 
121
- function WeatherWidget({ city }) {
122
- const { data, loading, error, execute } = useTool(weather);
123
-
124
- return (
125
- <div>
126
- <button onClick={() => execute({ city })}>Get Weather</button>
127
- {loading && <Spinner />}
128
- {error && <div>Error: {error.message}</div>}
129
- {data && <weather.View data={data} />}
130
- </div>
131
- );
132
- }
133
-
134
- // Or with auto-execution
135
- function WeatherWidget({ city }) {
136
- const { data, loading } = useTool(weather, { city }, { auto: true });
137
- return <weather.View data={data} loading={loading} />;
138
- }
261
+ const {
262
+ data, // Result data
263
+ loading, // Loading state
264
+ error, // Error if any
265
+ execute, // Execute function
266
+ reset, // Reset state
267
+ executed, // Has been executed
268
+ } = useTool(myTool, initialInput, {
269
+ auto: false,
270
+ onSuccess: (data) => {},
271
+ onError: (error) => {},
272
+ });
139
273
  ```
140
274
 
141
- ## AI Integration
142
-
143
- ### With Vercel AI SDK
275
+ ### `useToolStream(tool, options?)`
144
276
 
145
277
  ```typescript
146
- import { streamText } from 'ai';
147
- import { openai } from '@ai-sdk/openai';
148
-
149
- export async function POST(req: Request) {
150
- const { messages } = await req.json();
278
+ import { useToolStream } from '@lantos1618/better-ui';
151
279
 
152
- const result = await streamText({
153
- model: openai('gpt-4'),
154
- messages,
155
- tools: {
156
- weather: weather.toAITool(),
157
- search: search.toAITool(),
158
- },
159
- });
160
-
161
- return result.toDataStreamResponse();
162
- }
280
+ const {
281
+ data, // Progressive partial data
282
+ finalData, // Complete validated data (when done)
283
+ streaming, // True while receiving partial updates
284
+ loading, // True before first chunk
285
+ error,
286
+ execute,
287
+ reset,
288
+ } = useToolStream(myTool);
163
289
  ```
164
290
 
165
- ## API Reference
166
-
167
- ### `tool(config)` or `tool(name)`
168
-
169
- Create a new tool with object config or fluent builder.
291
+ ### `useTools(tools, options?)`
170
292
 
171
293
  ```typescript
172
- // Object config
173
- const t = tool({
174
- name: string,
175
- description?: string,
176
- input: ZodSchema,
177
- output?: ZodSchema,
178
- tags?: string[],
179
- cache?: { ttl: number, key?: (input) => string },
180
- });
181
-
182
- // Fluent builder
183
- const t = tool('name')
184
- .description(string)
185
- .input(ZodSchema)
186
- .output(ZodSchema)
187
- .tags(...string[])
188
- .cache({ ttl, key? });
189
- ```
294
+ import { useTools } from '@lantos1618/better-ui';
190
295
 
191
- ### `.server(handler)`
296
+ const tools = useTools({ weather, search });
192
297
 
193
- Define server-side implementation.
298
+ await tools.weather.execute({ city: 'London' });
299
+ await tools.search.execute({ query: 'restaurants' });
194
300
 
195
- ```typescript
196
- t.server(async (input, ctx) => {
197
- // ctx.env, ctx.headers, ctx.user, ctx.session available
198
- return result;
199
- });
301
+ tools.weather.data; // Weather result
302
+ tools.search.loading; // Search loading state
200
303
  ```
201
304
 
202
- ### `.client(handler)`
305
+ ## Chat Components
203
306
 
204
- Define client-side implementation (optional).
307
+ Import from `@lantos1618/better-ui/components`:
205
308
 
206
- ```typescript
207
- t.client(async (input, ctx) => {
208
- // ctx.cache, ctx.fetch, ctx.optimistic available
209
- return result;
210
- });
211
- ```
309
+ | Component | Description |
310
+ |-----------|-------------|
311
+ | `Chat` | All-in-one chat component (ChatProvider + Thread + Composer) |
312
+ | `ChatProvider` | Context provider wrapping AI SDK's `useChat` |
313
+ | `Thread` | Message list with auto-scroll |
314
+ | `Message` | Single message with automatic tool view rendering |
315
+ | `Composer` | Input form with send button |
316
+ | `ToolResult` | Renders a tool's `.View` in chat context |
212
317
 
213
- ### `.view(component)`
318
+ All components accept `className` for styling customization.
214
319
 
215
- Define React component for rendering results.
320
+ ## Providers
216
321
 
217
- ```typescript
218
- t.view((data, { loading, error }) => <Component data={data} />);
219
- ```
322
+ | Provider | Package Required | Example Model |
323
+ |----------|-----------------|---------------|
324
+ | OpenAI | `@ai-sdk/openai` (included) | `gpt-4o`, `gpt-5.2` |
325
+ | Anthropic | `@ai-sdk/anthropic` (optional) | `claude-4-sonnet` |
326
+ | Google | `@ai-sdk/google` (optional) | `gemini-2.5-pro` |
327
+ | OpenRouter | `@ai-sdk/openai` (included) | `anthropic/claude-4-sonnet` |
220
328
 
221
- ### `useTool(tool, input?, options?)`
222
-
223
- React hook for executing tools.
224
-
225
- ```typescript
226
- const {
227
- data, // Result data
228
- loading, // Loading state
229
- error, // Error if any
230
- execute, // Execute function
231
- reset, // Reset state
232
- executed, // Has been executed
233
- } = useTool(myTool, initialInput, {
234
- auto: false, // Auto-execute on mount
235
- onSuccess: (data) => {},
236
- onError: (error) => {},
237
- });
329
+ ```bash
330
+ # Optional providers
331
+ npm install @ai-sdk/anthropic # For Anthropic
332
+ npm install @ai-sdk/google # For Google Gemini
238
333
  ```
239
334
 
240
335
  ## Project Structure
241
336
 
242
337
  ```
243
338
  src/
244
- tool.tsx # Core tool() API
339
+ tool.tsx # Core tool() API with streaming
245
340
  react/
246
- useTool.ts # React hook
247
- index.ts # Main exports
248
-
249
- app/
250
- demo/ # Demo page
251
- api/chat/ # Chat API route
252
- api/tools/ # Tool execution API
253
-
254
- lib/
255
- tools.tsx # Example tool definitions
256
-
257
- docs/
258
- API_V2.md # Full API documentation
341
+ useTool.ts # React hooks (useTool, useTools)
342
+ useToolStream.ts # Streaming hook
343
+ components/
344
+ Chat.tsx # All-in-one chat component
345
+ ChatProvider.tsx # Chat context provider
346
+ Thread.tsx # Message list
347
+ Message.tsx # Single message
348
+ Composer.tsx # Input form
349
+ ToolResult.tsx # Tool view renderer
350
+ providers/
351
+ openai.ts # OpenAI adapter
352
+ anthropic.ts # Anthropic adapter
353
+ google.ts # Google Gemini adapter
354
+ openrouter.ts # OpenRouter adapter
355
+ adapters/
356
+ nextjs.ts # Next.js route handlers
357
+ express.ts # Express middleware
358
+ index.ts # Main exports
259
359
  ```
260
360
 
261
361
  ## Development
@@ -266,6 +366,7 @@ npm run dev # Run dev server
266
366
  npm run build:lib # Build library
267
367
  npm run build # Build everything
268
368
  npm run type-check # TypeScript check
369
+ npm test # Run tests
269
370
  ```
270
371
 
271
372
  ## License
@@ -0,0 +1,187 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import React from 'react';
3
+ import { UIMessage, ChatStatus } from 'ai';
4
+ import { T as Tool } from './tool-Ca2x-VNK.mjs';
5
+ import { P as PersistenceAdapter, T as Thread$1 } from './types-CAOfGUPH.mjs';
6
+
7
+ interface ToolStateEntry {
8
+ output: unknown;
9
+ loading: boolean;
10
+ error: string | null;
11
+ version: number;
12
+ toolName?: string;
13
+ /** HITL confirmation status */
14
+ status?: 'pending' | 'confirmed' | 'rejected';
15
+ /** Entity group key — "toolName:groupKey(input)" — groups related calls */
16
+ entityId?: string;
17
+ /** Raw tool input, stored for conditional confirm checks */
18
+ toolInput?: unknown;
19
+ /** Auto-incrementing insertion order (store-assigned) */
20
+ seqNo?: number;
21
+ }
22
+ interface ToolStateStore {
23
+ get: (toolCallId: string) => ToolStateEntry | undefined;
24
+ set: (toolCallId: string, entry: ToolStateEntry) => void;
25
+ /** Remove all entries (e.g. when switching threads) */
26
+ clear: () => void;
27
+ subscribe: (toolCallId: string, listener: () => void) => () => void;
28
+ subscribeAll: (listener: () => void) => () => void;
29
+ getSnapshot: () => Map<string, ToolStateEntry>;
30
+ /** Returns Map with highest-seqNo entry per entityId + all ungrouped entries */
31
+ getLatestPerEntity: () => Map<string, ToolStateEntry>;
32
+ /** Returns the oldest (lowest seqNo) entry with the given entityId — the "anchor" */
33
+ findAnchor: (entityId: string) => {
34
+ toolCallId: string;
35
+ entry: ToolStateEntry;
36
+ } | undefined;
37
+ }
38
+ declare function createToolStateStore(): ToolStateStore;
39
+ declare function useToolState(store: ToolStateStore, toolCallId: string): ToolStateEntry | undefined;
40
+
41
+ /** Parsed tool part from a UIMessage */
42
+ interface ToolPartInfo {
43
+ toolName: string;
44
+ toolCallId: string;
45
+ state: string;
46
+ output: unknown;
47
+ }
48
+ interface ChatContextValue {
49
+ messages: UIMessage[];
50
+ sendMessage: (text: string) => void;
51
+ isLoading: boolean;
52
+ status: ChatStatus;
53
+ tools: Record<string, Tool>;
54
+ executeToolDirect: (toolName: string, toolInput: Record<string, unknown>, toolCallId: string) => Promise<void>;
55
+ getOnAction: (toolCallId: string, toolName: string) => (input: Record<string, unknown>) => void;
56
+ toolStateStore: ToolStateStore;
57
+ /** Approve and execute a HITL tool, then feed result back to AI */
58
+ confirmTool: (toolCallId: string, toolName: string, toolInput: Record<string, unknown>) => Promise<void>;
59
+ /** Reject a HITL tool and notify AI */
60
+ rejectTool: (toolCallId: string, toolName: string) => Promise<void>;
61
+ /** Retry a failed tool execution */
62
+ retryTool: (toolCallId: string, toolName: string, toolInput: unknown) => void;
63
+ /** Available threads (only when persistence is configured) */
64
+ threads?: Thread$1[];
65
+ /** Current thread ID */
66
+ threadId?: string;
67
+ /** Create a new thread (only when persistence is configured) */
68
+ createThread?: (title?: string) => Promise<Thread$1>;
69
+ /** Switch to a different thread (only when persistence is configured) */
70
+ switchThread?: (threadId: string) => Promise<void>;
71
+ /** Delete a thread (only when persistence is configured) */
72
+ deleteThread?: (threadId: string) => Promise<void>;
73
+ }
74
+ interface ChatProviderProps {
75
+ endpoint?: string;
76
+ tools: Record<string, Tool>;
77
+ toolStateStore?: ToolStateStore;
78
+ /** Persistence adapter for thread/message storage */
79
+ persistence?: PersistenceAdapter;
80
+ /** Active thread ID (used with persistence) */
81
+ threadId?: string;
82
+ children: React.ReactNode;
83
+ }
84
+ /**
85
+ * Hook to access chat context. Must be used within a ChatProvider.
86
+ */
87
+ declare function useChatContext(): ChatContextValue;
88
+ declare function ChatProvider({ endpoint, tools, toolStateStore: externalStore, persistence, threadId, children }: ChatProviderProps): react_jsx_runtime.JSX.Element;
89
+
90
+ interface ThreadProps {
91
+ className?: string;
92
+ emptyMessage?: string;
93
+ suggestions?: string[];
94
+ }
95
+ /**
96
+ * Message list with auto-scroll.
97
+ * Renders messages from ChatContext, maps over messages, renders text parts and tool parts.
98
+ */
99
+ declare function Thread({ className, emptyMessage, suggestions }: ThreadProps): react_jsx_runtime.JSX.Element;
100
+
101
+ interface MessageProps {
102
+ message: UIMessage;
103
+ tools: Record<string, Tool>;
104
+ toolStateStore: ToolStateStore;
105
+ getOnAction: (toolCallId: string, toolName: string) => (input: Record<string, unknown>) => void;
106
+ onConfirm?: (toolCallId: string, toolName: string, toolInput: Record<string, unknown>) => void;
107
+ onReject?: (toolCallId: string, toolName: string) => void;
108
+ onRetry?: (toolCallId: string, toolName: string, toolInput: unknown) => void;
109
+ className?: string;
110
+ }
111
+ /**
112
+ * Renders a single UIMessage.
113
+ * - User messages: right-aligned bubble (plain text)
114
+ * - Assistant messages: left-aligned bubble (rendered markdown)
115
+ * - Tool parts: renders tool.View automatically
116
+ */
117
+ declare function Message({ message, tools, toolStateStore, getOnAction, onConfirm, onReject, onRetry, className }: MessageProps): react_jsx_runtime.JSX.Element | null;
118
+
119
+ interface ComposerProps {
120
+ className?: string;
121
+ placeholder?: string;
122
+ }
123
+ /**
124
+ * Input form with text input and send button.
125
+ * Uses ChatContext to send messages. Disabled during loading.
126
+ */
127
+ declare function Composer({ className, placeholder }: ComposerProps): react_jsx_runtime.JSX.Element;
128
+
129
+ interface ToolResultProps {
130
+ toolName: string;
131
+ toolCallId: string;
132
+ output: unknown;
133
+ toolInput?: unknown;
134
+ hasResult: boolean;
135
+ /** Tool part state from the AI SDK (e.g. 'partial-call', 'call', 'output-available') */
136
+ toolPartState?: string;
137
+ toolStateStore: ToolStateStore;
138
+ tools: Record<string, Tool>;
139
+ getOnAction: (toolCallId: string, toolName: string) => (input: Record<string, unknown>) => void;
140
+ onConfirm?: (toolCallId: string, toolName: string, toolInput: Record<string, unknown>) => void;
141
+ onReject?: (toolCallId: string, toolName: string) => void;
142
+ onRetry?: (toolCallId: string, toolName: string, toolInput: unknown) => void;
143
+ className?: string;
144
+ }
145
+ /**
146
+ * Renders a tool's View component given tool name and output.
147
+ * Uses the tool state store for in-place updates (e.g. counter clicks),
148
+ * falling back to message part state.
149
+ *
150
+ * For HITL tools (confirm: true), renders a confirmation card when
151
+ * the tool is awaiting user approval.
152
+ *
153
+ * Supports groupKey-based in-place updates: when an older call (anchor) with
154
+ * the same entityId exists, followup calls update the anchor's data and render nothing.
155
+ */
156
+ declare function ToolResult({ toolName, toolCallId, output, toolInput, hasResult, toolPartState, toolStateStore, tools, getOnAction, onConfirm, onReject, onRetry, className, }: ToolResultProps): react_jsx_runtime.JSX.Element | null;
157
+
158
+ interface ChatProps {
159
+ endpoint?: string;
160
+ tools: Record<string, Tool>;
161
+ className?: string;
162
+ placeholder?: string;
163
+ emptyMessage?: string;
164
+ suggestions?: string[];
165
+ }
166
+ /**
167
+ * Convenience all-in-one chat component.
168
+ * Combines ChatProvider + Thread + Composer into a single drop-in component.
169
+ */
170
+ declare function Chat({ endpoint, tools, className, placeholder, emptyMessage, suggestions, }: ChatProps): react_jsx_runtime.JSX.Element;
171
+
172
+ interface ThemeProviderProps {
173
+ /** Theme name — sets data-theme attribute. Default: 'dark' */
174
+ theme?: string;
175
+ /** Override individual CSS variables */
176
+ variables?: Record<string, string>;
177
+ /** Additional CSS class */
178
+ className?: string;
179
+ children: React.ReactNode;
180
+ }
181
+ /**
182
+ * Wraps children with a themed container.
183
+ * Sets `data-theme` for CSS variable scoping and applies inline variable overrides.
184
+ */
185
+ declare function ThemeProvider({ theme, variables, className, children, }: ThemeProviderProps): react_jsx_runtime.JSX.Element;
186
+
187
+ export { type ChatProviderProps as C, type MessageProps as M, type ToolPartInfo as T, type ThreadProps as a, type ComposerProps as b, type ToolResultProps as c, type ChatProps as d, type ThemeProviderProps as e, type ToolStateStore as f, type ToolStateEntry as g, ChatProvider as h, Thread as i, Message as j, Composer as k, ToolResult as l, Chat as m, createToolStateStore as n, useToolState as o, ThemeProvider as p, useChatContext as u };