@lantos1618/better-ui 0.1.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/LICENSE +21 -0
- package/README.md +190 -0
- package/lib/aui/README.md +136 -0
- package/lib/aui/__tests__/aui-complete.test.ts +251 -0
- package/lib/aui/__tests__/aui-comprehensive.test.ts +376 -0
- package/lib/aui/__tests__/aui-concise.test.ts +278 -0
- package/lib/aui/__tests__/aui-integration.test.ts +309 -0
- package/lib/aui/__tests__/aui-simple.test.ts +116 -0
- package/lib/aui/__tests__/aui.test.ts +269 -0
- package/lib/aui/__tests__/concise-api.test.ts +165 -0
- package/lib/aui/__tests__/core.test.ts +265 -0
- package/lib/aui/__tests__/simple-api.test.ts +200 -0
- package/lib/aui/ai-assistant.ts +408 -0
- package/lib/aui/ai-control.ts +353 -0
- package/lib/aui/client/use-aui.ts +55 -0
- package/lib/aui/client-control.ts +551 -0
- package/lib/aui/client-executor.ts +417 -0
- package/lib/aui/components/ToolRenderer.tsx +22 -0
- package/lib/aui/core.ts +137 -0
- package/lib/aui/demo.tsx +89 -0
- package/lib/aui/examples/ai-complete-demo.tsx +359 -0
- package/lib/aui/examples/ai-control-demo.tsx +356 -0
- package/lib/aui/examples/ai-control-tools.ts +308 -0
- package/lib/aui/examples/concise-api.tsx +153 -0
- package/lib/aui/examples/index.tsx +163 -0
- package/lib/aui/examples/quick-demo.tsx +91 -0
- package/lib/aui/examples/simple-demo.tsx +71 -0
- package/lib/aui/examples/simple-tools.tsx +160 -0
- package/lib/aui/examples/user-api.tsx +208 -0
- package/lib/aui/examples/user-requested.tsx +174 -0
- package/lib/aui/examples/weather-search-tools.tsx +119 -0
- package/lib/aui/examples.tsx +367 -0
- package/lib/aui/hooks/useAUITool.ts +142 -0
- package/lib/aui/hooks/useAUIToolEnhanced.ts +343 -0
- package/lib/aui/hooks/useAUITools.ts +195 -0
- package/lib/aui/index.ts +156 -0
- package/lib/aui/provider.tsx +45 -0
- package/lib/aui/server-control.ts +386 -0
- package/lib/aui/server-executor.ts +165 -0
- package/lib/aui/server.ts +167 -0
- package/lib/aui/tool-registry.ts +380 -0
- package/lib/aui/tools/advanced-examples.tsx +86 -0
- package/lib/aui/tools/ai-complete.ts +375 -0
- package/lib/aui/tools/api-tools.tsx +230 -0
- package/lib/aui/tools/data-tools.tsx +232 -0
- package/lib/aui/tools/dom-tools.tsx +202 -0
- package/lib/aui/tools/examples.ts +43 -0
- package/lib/aui/tools/file-tools.tsx +202 -0
- package/lib/aui/tools/form-tools.tsx +233 -0
- package/lib/aui/tools/index.ts +8 -0
- package/lib/aui/tools/navigation-tools.tsx +172 -0
- package/lib/aui/tools/notification-tools.ts +213 -0
- package/lib/aui/tools/state-tools.tsx +209 -0
- package/lib/aui/types.ts +47 -0
- package/lib/aui/vercel-ai.ts +100 -0
- package/package.json +51 -0
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import { ReactElement } from 'react';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { AUITool, AUIContext } from './core';
|
|
4
|
+
|
|
5
|
+
export interface AIControlOptions {
|
|
6
|
+
permissions?: {
|
|
7
|
+
allowClientExecution?: boolean;
|
|
8
|
+
allowServerExecution?: boolean;
|
|
9
|
+
allowDatabaseAccess?: boolean;
|
|
10
|
+
allowFileSystemAccess?: boolean;
|
|
11
|
+
allowNetworkRequests?: boolean;
|
|
12
|
+
};
|
|
13
|
+
rateLimit?: {
|
|
14
|
+
requestsPerMinute?: number;
|
|
15
|
+
requestsPerHour?: number;
|
|
16
|
+
};
|
|
17
|
+
audit?: boolean;
|
|
18
|
+
sandbox?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class AIControlledTool<TInput = any, TOutput = any> extends AUITool<TInput, TOutput> {
|
|
22
|
+
private controlOptions: AIControlOptions = {};
|
|
23
|
+
private executionLog: Array<{
|
|
24
|
+
timestamp: Date;
|
|
25
|
+
input: TInput;
|
|
26
|
+
output?: TOutput;
|
|
27
|
+
error?: Error;
|
|
28
|
+
context?: Partial<AUIContext>;
|
|
29
|
+
}> = [];
|
|
30
|
+
|
|
31
|
+
constructor(name: string, options?: AIControlOptions) {
|
|
32
|
+
super(name);
|
|
33
|
+
this.controlOptions = options || {};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Override methods to maintain correct type
|
|
37
|
+
input<T>(schema: z.ZodType<T>): AIControlledTool<T, TOutput> {
|
|
38
|
+
super.input(schema);
|
|
39
|
+
return this as any;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
execute<O>(handler: (params: { input: TInput; ctx?: AUIContext }) => O | Promise<O>): AIControlledTool<TInput, O> {
|
|
43
|
+
super.execute(handler);
|
|
44
|
+
return this as any;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
clientExecute(handler: (params: { input: TInput; ctx: AUIContext }) => TOutput | Promise<TOutput>): this {
|
|
48
|
+
super.clientExecute(handler);
|
|
49
|
+
return this;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
render(component: (props: { data: TOutput; input?: TInput; loading?: boolean; error?: Error }) => ReactElement): this {
|
|
53
|
+
super.render(component);
|
|
54
|
+
return this;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
middleware(fn: (params: { input: TInput; ctx: AUIContext; next: () => Promise<TOutput> }) => Promise<TOutput>): this {
|
|
58
|
+
super.middleware(fn);
|
|
59
|
+
return this;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
describe(description: string): this {
|
|
63
|
+
super.describe(description);
|
|
64
|
+
return this;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
tag(...tags: string[]): this {
|
|
68
|
+
super.tag(...tags);
|
|
69
|
+
return this;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
withPermissions(permissions: AIControlOptions['permissions']): this {
|
|
73
|
+
this.controlOptions.permissions = {
|
|
74
|
+
...this.controlOptions.permissions,
|
|
75
|
+
...permissions
|
|
76
|
+
};
|
|
77
|
+
return this;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
withRateLimit(limit: AIControlOptions['rateLimit']): this {
|
|
81
|
+
this.controlOptions.rateLimit = limit;
|
|
82
|
+
return this;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
enableAudit(): this {
|
|
86
|
+
this.controlOptions.audit = true;
|
|
87
|
+
return this;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
enableSandbox(): this {
|
|
91
|
+
this.controlOptions.sandbox = true;
|
|
92
|
+
return this;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async run(input: TInput, ctx?: AUIContext): Promise<TOutput> {
|
|
96
|
+
const startTime = Date.now();
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
this.validatePermissions(ctx);
|
|
100
|
+
await this.checkRateLimit();
|
|
101
|
+
|
|
102
|
+
const result = await super.run(input, ctx);
|
|
103
|
+
|
|
104
|
+
if (this.controlOptions.audit) {
|
|
105
|
+
this.executionLog.push({
|
|
106
|
+
timestamp: new Date(),
|
|
107
|
+
input,
|
|
108
|
+
output: result,
|
|
109
|
+
context: ctx ? { user: ctx.user, session: ctx.session } : undefined
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return result;
|
|
114
|
+
} catch (error) {
|
|
115
|
+
if (this.controlOptions.audit) {
|
|
116
|
+
this.executionLog.push({
|
|
117
|
+
timestamp: new Date(),
|
|
118
|
+
input,
|
|
119
|
+
error: error as Error,
|
|
120
|
+
context: ctx ? { user: ctx.user, session: ctx.session } : undefined
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private validatePermissions(ctx?: AUIContext): void {
|
|
128
|
+
if (!this.controlOptions.permissions) return;
|
|
129
|
+
|
|
130
|
+
const isClient = !ctx?.isServer;
|
|
131
|
+
const perms = this.controlOptions.permissions;
|
|
132
|
+
|
|
133
|
+
if (isClient && !perms.allowClientExecution) {
|
|
134
|
+
throw new Error('Client execution not allowed for this tool');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (ctx?.isServer && !perms.allowServerExecution) {
|
|
138
|
+
throw new Error('Server execution not allowed for this tool');
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private async checkRateLimit(): Promise<void> {
|
|
143
|
+
if (!this.controlOptions.rateLimit) return;
|
|
144
|
+
|
|
145
|
+
const now = Date.now();
|
|
146
|
+
const { requestsPerMinute, requestsPerHour } = this.controlOptions.rateLimit;
|
|
147
|
+
|
|
148
|
+
if (requestsPerMinute) {
|
|
149
|
+
const recentMinute = this.executionLog.filter(
|
|
150
|
+
log => now - log.timestamp.getTime() < 60000
|
|
151
|
+
);
|
|
152
|
+
if (recentMinute.length >= requestsPerMinute) {
|
|
153
|
+
throw new Error('Rate limit exceeded (per minute)');
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (requestsPerHour) {
|
|
158
|
+
const recentHour = this.executionLog.filter(
|
|
159
|
+
log => now - log.timestamp.getTime() < 3600000
|
|
160
|
+
);
|
|
161
|
+
if (recentHour.length >= requestsPerHour) {
|
|
162
|
+
throw new Error('Rate limit exceeded (per hour)');
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
getExecutionLog() {
|
|
168
|
+
return [...this.executionLog];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
clearExecutionLog() {
|
|
172
|
+
this.executionLog = [];
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function createAITool(name: string, options?: AIControlOptions): AIControlledTool {
|
|
177
|
+
return new AIControlledTool(name, options);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export const aiTools = {
|
|
181
|
+
fileSystem: createAITool('fs-control', {
|
|
182
|
+
permissions: {
|
|
183
|
+
allowServerExecution: true,
|
|
184
|
+
allowFileSystemAccess: true
|
|
185
|
+
},
|
|
186
|
+
audit: true
|
|
187
|
+
})
|
|
188
|
+
.input(z.object({
|
|
189
|
+
action: z.enum(['read', 'write', 'delete', 'list']),
|
|
190
|
+
path: z.string(),
|
|
191
|
+
content: z.string().optional()
|
|
192
|
+
}))
|
|
193
|
+
.execute(async ({ input }) => {
|
|
194
|
+
switch (input.action) {
|
|
195
|
+
case 'read':
|
|
196
|
+
return { content: 'File content here' };
|
|
197
|
+
case 'write':
|
|
198
|
+
return { success: true, path: input.path };
|
|
199
|
+
case 'delete':
|
|
200
|
+
return { success: true, deleted: input.path };
|
|
201
|
+
case 'list':
|
|
202
|
+
return { files: ['file1.txt', 'file2.txt'] };
|
|
203
|
+
default:
|
|
204
|
+
throw new Error('Unknown action');
|
|
205
|
+
}
|
|
206
|
+
}),
|
|
207
|
+
|
|
208
|
+
database: createAITool('db-control', {
|
|
209
|
+
permissions: {
|
|
210
|
+
allowServerExecution: true,
|
|
211
|
+
allowDatabaseAccess: true
|
|
212
|
+
},
|
|
213
|
+
rateLimit: {
|
|
214
|
+
requestsPerMinute: 30,
|
|
215
|
+
requestsPerHour: 500
|
|
216
|
+
},
|
|
217
|
+
audit: true
|
|
218
|
+
})
|
|
219
|
+
.input(z.object({
|
|
220
|
+
query: z.string(),
|
|
221
|
+
params: z.array(z.any()).optional()
|
|
222
|
+
}))
|
|
223
|
+
.execute(async ({ input }) => {
|
|
224
|
+
return {
|
|
225
|
+
rows: [],
|
|
226
|
+
query: input.query,
|
|
227
|
+
executed: new Date().toISOString()
|
|
228
|
+
};
|
|
229
|
+
}),
|
|
230
|
+
|
|
231
|
+
uiManipulation: createAITool('ui-control', {
|
|
232
|
+
permissions: {
|
|
233
|
+
allowClientExecution: true
|
|
234
|
+
},
|
|
235
|
+
audit: true
|
|
236
|
+
})
|
|
237
|
+
.input(z.object({
|
|
238
|
+
selector: z.string(),
|
|
239
|
+
action: z.enum(['click', 'type', 'focus', 'scroll']),
|
|
240
|
+
value: z.string().optional()
|
|
241
|
+
}))
|
|
242
|
+
.clientExecute(async ({ input }) => {
|
|
243
|
+
const element = document.querySelector(input.selector);
|
|
244
|
+
if (!element) throw new Error(`Element not found: ${input.selector}`);
|
|
245
|
+
|
|
246
|
+
switch (input.action) {
|
|
247
|
+
case 'click':
|
|
248
|
+
(element as HTMLElement).click();
|
|
249
|
+
break;
|
|
250
|
+
case 'type':
|
|
251
|
+
if (element instanceof HTMLInputElement && input.value) {
|
|
252
|
+
element.value = input.value;
|
|
253
|
+
element.dispatchEvent(new Event('input', { bubbles: true }));
|
|
254
|
+
}
|
|
255
|
+
break;
|
|
256
|
+
case 'focus':
|
|
257
|
+
(element as HTMLElement).focus();
|
|
258
|
+
break;
|
|
259
|
+
case 'scroll':
|
|
260
|
+
element.scrollIntoView({ behavior: 'smooth' });
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return { success: true, action: input.action, selector: input.selector };
|
|
265
|
+
}),
|
|
266
|
+
|
|
267
|
+
apiCall: createAITool('api-control', {
|
|
268
|
+
permissions: {
|
|
269
|
+
allowNetworkRequests: true
|
|
270
|
+
},
|
|
271
|
+
rateLimit: {
|
|
272
|
+
requestsPerMinute: 20
|
|
273
|
+
},
|
|
274
|
+
audit: true
|
|
275
|
+
})
|
|
276
|
+
.input(z.object({
|
|
277
|
+
url: z.string().url(),
|
|
278
|
+
method: z.enum(['GET', 'POST', 'PUT', 'DELETE']),
|
|
279
|
+
headers: z.record(z.string()).optional(),
|
|
280
|
+
body: z.any().optional()
|
|
281
|
+
}))
|
|
282
|
+
.execute(async ({ input, ctx }) => {
|
|
283
|
+
const response = await ctx!.fetch(input.url, {
|
|
284
|
+
method: input.method,
|
|
285
|
+
headers: input.headers,
|
|
286
|
+
body: input.body ? JSON.stringify(input.body) : undefined
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
return {
|
|
290
|
+
status: response.status,
|
|
291
|
+
data: await response.json().catch(() => null),
|
|
292
|
+
headers: Object.fromEntries(response.headers.entries())
|
|
293
|
+
};
|
|
294
|
+
})
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
export function createAIControlSystem() {
|
|
298
|
+
const tools = new Map<string, AIControlledTool>();
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
register(tool: AIControlledTool) {
|
|
302
|
+
tools.set(tool.name, tool);
|
|
303
|
+
return this;
|
|
304
|
+
},
|
|
305
|
+
|
|
306
|
+
get(name: string) {
|
|
307
|
+
return tools.get(name);
|
|
308
|
+
},
|
|
309
|
+
|
|
310
|
+
clear() {
|
|
311
|
+
tools.clear();
|
|
312
|
+
},
|
|
313
|
+
|
|
314
|
+
async execute(name: string, input: any, ctx?: AUIContext) {
|
|
315
|
+
const tool = tools.get(name);
|
|
316
|
+
if (!tool) throw new Error(`Tool ${name} not found`);
|
|
317
|
+
return await tool.run(input, ctx);
|
|
318
|
+
},
|
|
319
|
+
|
|
320
|
+
getAuditLog(toolName?: string) {
|
|
321
|
+
if (toolName) {
|
|
322
|
+
const tool = tools.get(toolName);
|
|
323
|
+
return tool ? tool.getExecutionLog() : [];
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const allLogs: any[] = [];
|
|
327
|
+
tools.forEach((tool, name) => {
|
|
328
|
+
const logs = tool.getExecutionLog();
|
|
329
|
+
logs.forEach(log => allLogs.push({ tool: name, ...log }));
|
|
330
|
+
});
|
|
331
|
+
return allLogs.sort((a, b) =>
|
|
332
|
+
b.timestamp.getTime() - a.timestamp.getTime()
|
|
333
|
+
);
|
|
334
|
+
},
|
|
335
|
+
|
|
336
|
+
listTools() {
|
|
337
|
+
return Array.from(tools.values()).map(tool => ({
|
|
338
|
+
name: tool.name,
|
|
339
|
+
description: tool.description,
|
|
340
|
+
tags: tool.tags,
|
|
341
|
+
config: tool.toJSON()
|
|
342
|
+
}));
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export const aiControlSystem = createAIControlSystem();
|
|
348
|
+
|
|
349
|
+
Object.values(aiTools).forEach(tool => {
|
|
350
|
+
aiControlSystem.register(tool);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
export default aiControlSystem;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback } from 'react';
|
|
4
|
+
import { AUITool, AUIContext } from '../index';
|
|
5
|
+
|
|
6
|
+
interface UseAUIOptions {
|
|
7
|
+
cache?: boolean;
|
|
8
|
+
context?: Partial<AUIContext>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface UseAUIResult<TInput, TOutput> {
|
|
12
|
+
execute: (input: TInput) => Promise<void>;
|
|
13
|
+
data: TOutput | null;
|
|
14
|
+
loading: boolean;
|
|
15
|
+
error: Error | null;
|
|
16
|
+
reset: () => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function useAUI<TInput = any, TOutput = any>(
|
|
20
|
+
tool: AUITool<TInput, TOutput>,
|
|
21
|
+
options: UseAUIOptions = {}
|
|
22
|
+
): UseAUIResult<TInput, TOutput> {
|
|
23
|
+
const [data, setData] = useState<TOutput | null>(null);
|
|
24
|
+
const [loading, setLoading] = useState(false);
|
|
25
|
+
const [error, setError] = useState<Error | null>(null);
|
|
26
|
+
|
|
27
|
+
const execute = useCallback(async (input: TInput) => {
|
|
28
|
+
setLoading(true);
|
|
29
|
+
setError(null);
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const context: AUIContext = {
|
|
33
|
+
cache: options.cache ? new Map() : new Map(),
|
|
34
|
+
fetch: globalThis.fetch,
|
|
35
|
+
isServer: false,
|
|
36
|
+
...options.context
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const result = await tool.run(input, context);
|
|
40
|
+
setData(result);
|
|
41
|
+
} catch (err) {
|
|
42
|
+
setError(err instanceof Error ? err : new Error('Unknown error'));
|
|
43
|
+
} finally {
|
|
44
|
+
setLoading(false);
|
|
45
|
+
}
|
|
46
|
+
}, [tool, options.cache, options.context]);
|
|
47
|
+
|
|
48
|
+
const reset = useCallback(() => {
|
|
49
|
+
setData(null);
|
|
50
|
+
setError(null);
|
|
51
|
+
setLoading(false);
|
|
52
|
+
}, []);
|
|
53
|
+
|
|
54
|
+
return { execute, data, loading, error, reset };
|
|
55
|
+
}
|