@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,551 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { createAITool, AIControlledTool } from './ai-control';
|
|
3
|
+
import { AUIContext } from './core';
|
|
4
|
+
|
|
5
|
+
export interface ClientControlContext extends AUIContext {
|
|
6
|
+
window?: Window;
|
|
7
|
+
document?: Document;
|
|
8
|
+
navigator?: Navigator;
|
|
9
|
+
localStorage?: Storage;
|
|
10
|
+
sessionStorage?: Storage;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const clientTools = {
|
|
14
|
+
domManipulation: createAITool('client-dom', {
|
|
15
|
+
permissions: {
|
|
16
|
+
allowClientExecution: true
|
|
17
|
+
},
|
|
18
|
+
audit: true
|
|
19
|
+
})
|
|
20
|
+
.input(z.object({
|
|
21
|
+
operation: z.enum(['select', 'create', 'update', 'remove', 'addClass', 'removeClass', 'setAttribute', 'style']),
|
|
22
|
+
selector: z.string().optional(),
|
|
23
|
+
element: z.string().optional(),
|
|
24
|
+
content: z.string().optional(),
|
|
25
|
+
attributes: z.record(z.string()).optional(),
|
|
26
|
+
styles: z.record(z.string()).optional(),
|
|
27
|
+
classes: z.array(z.string()).optional()
|
|
28
|
+
}))
|
|
29
|
+
.describe('Manipulate DOM elements on the client')
|
|
30
|
+
.tag('client', 'dom', 'ui')
|
|
31
|
+
.clientExecute(async ({ input }) => {
|
|
32
|
+
switch (input.operation) {
|
|
33
|
+
case 'select':
|
|
34
|
+
if (!input.selector) throw new Error('Selector required');
|
|
35
|
+
const elements = document.querySelectorAll(input.selector);
|
|
36
|
+
return {
|
|
37
|
+
count: elements.length,
|
|
38
|
+
elements: Array.from(elements).map(el => ({
|
|
39
|
+
tagName: el.tagName,
|
|
40
|
+
id: el.id,
|
|
41
|
+
classes: Array.from(el.classList)
|
|
42
|
+
}))
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
case 'create':
|
|
46
|
+
if (!input.element) throw new Error('Element type required');
|
|
47
|
+
const newEl = document.createElement(input.element);
|
|
48
|
+
if (input.content) newEl.textContent = input.content;
|
|
49
|
+
if (input.attributes) {
|
|
50
|
+
Object.entries(input.attributes).forEach(([key, value]) => {
|
|
51
|
+
newEl.setAttribute(key, value);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
if (input.styles) {
|
|
55
|
+
Object.assign(newEl.style, input.styles);
|
|
56
|
+
}
|
|
57
|
+
if (input.selector) {
|
|
58
|
+
const parent = document.querySelector(input.selector);
|
|
59
|
+
if (parent) parent.appendChild(newEl);
|
|
60
|
+
}
|
|
61
|
+
return { success: true, created: input.element };
|
|
62
|
+
|
|
63
|
+
case 'update':
|
|
64
|
+
if (!input.selector) throw new Error('Selector required');
|
|
65
|
+
const updateEls = document.querySelectorAll(input.selector);
|
|
66
|
+
updateEls.forEach(el => {
|
|
67
|
+
if (input.content !== undefined) el.textContent = input.content;
|
|
68
|
+
if (input.attributes) {
|
|
69
|
+
Object.entries(input.attributes).forEach(([key, value]) => {
|
|
70
|
+
el.setAttribute(key, value);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
if (input.styles) {
|
|
74
|
+
Object.assign((el as HTMLElement).style, input.styles);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
return { success: true, updated: updateEls.length };
|
|
78
|
+
|
|
79
|
+
case 'remove':
|
|
80
|
+
if (!input.selector) throw new Error('Selector required');
|
|
81
|
+
const removeEls = document.querySelectorAll(input.selector);
|
|
82
|
+
removeEls.forEach(el => el.remove());
|
|
83
|
+
return { success: true, removed: removeEls.length };
|
|
84
|
+
|
|
85
|
+
case 'addClass':
|
|
86
|
+
if (!input.selector || !input.classes) throw new Error('Selector and classes required');
|
|
87
|
+
const addClassEls = document.querySelectorAll(input.selector);
|
|
88
|
+
addClassEls.forEach(el => el.classList.add(...input.classes!));
|
|
89
|
+
return { success: true, modified: addClassEls.length };
|
|
90
|
+
|
|
91
|
+
case 'removeClass':
|
|
92
|
+
if (!input.selector || !input.classes) throw new Error('Selector and classes required');
|
|
93
|
+
const removeClassEls = document.querySelectorAll(input.selector);
|
|
94
|
+
removeClassEls.forEach(el => el.classList.remove(...input.classes!));
|
|
95
|
+
return { success: true, modified: removeClassEls.length };
|
|
96
|
+
|
|
97
|
+
case 'setAttribute':
|
|
98
|
+
if (!input.selector || !input.attributes) throw new Error('Selector and attributes required');
|
|
99
|
+
const setAttrEls = document.querySelectorAll(input.selector);
|
|
100
|
+
setAttrEls.forEach(el => {
|
|
101
|
+
Object.entries(input.attributes!).forEach(([key, value]) => {
|
|
102
|
+
el.setAttribute(key, value);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
return { success: true, modified: setAttrEls.length };
|
|
106
|
+
|
|
107
|
+
case 'style':
|
|
108
|
+
if (!input.selector || !input.styles) throw new Error('Selector and styles required');
|
|
109
|
+
const styleEls = document.querySelectorAll(input.selector);
|
|
110
|
+
styleEls.forEach(el => {
|
|
111
|
+
Object.assign((el as HTMLElement).style, input.styles);
|
|
112
|
+
});
|
|
113
|
+
return { success: true, styled: styleEls.length };
|
|
114
|
+
|
|
115
|
+
default:
|
|
116
|
+
throw new Error('Unknown DOM operation');
|
|
117
|
+
}
|
|
118
|
+
}),
|
|
119
|
+
|
|
120
|
+
eventHandling: createAITool('client-events', {
|
|
121
|
+
permissions: {
|
|
122
|
+
allowClientExecution: true
|
|
123
|
+
},
|
|
124
|
+
audit: true
|
|
125
|
+
})
|
|
126
|
+
.input(z.object({
|
|
127
|
+
action: z.enum(['trigger', 'listen', 'remove']),
|
|
128
|
+
selector: z.string(),
|
|
129
|
+
event: z.string(),
|
|
130
|
+
data: z.any().optional(),
|
|
131
|
+
options: z.object({
|
|
132
|
+
bubbles: z.boolean().optional(),
|
|
133
|
+
cancelable: z.boolean().optional(),
|
|
134
|
+
once: z.boolean().optional()
|
|
135
|
+
}).optional()
|
|
136
|
+
}))
|
|
137
|
+
.describe('Handle client-side events')
|
|
138
|
+
.tag('client', 'events', 'interaction')
|
|
139
|
+
.clientExecute(async ({ input }) => {
|
|
140
|
+
const element = document.querySelector(input.selector);
|
|
141
|
+
if (!element) throw new Error(`Element not found: ${input.selector}`);
|
|
142
|
+
|
|
143
|
+
switch (input.action) {
|
|
144
|
+
case 'trigger':
|
|
145
|
+
const event = new CustomEvent(input.event, {
|
|
146
|
+
detail: input.data,
|
|
147
|
+
bubbles: input.options?.bubbles ?? true,
|
|
148
|
+
cancelable: input.options?.cancelable ?? true
|
|
149
|
+
});
|
|
150
|
+
element.dispatchEvent(event);
|
|
151
|
+
return { success: true, triggered: input.event };
|
|
152
|
+
|
|
153
|
+
case 'listen':
|
|
154
|
+
const handler = (e: Event) => {
|
|
155
|
+
console.log('Event triggered:', input.event, e);
|
|
156
|
+
};
|
|
157
|
+
element.addEventListener(input.event, handler, {
|
|
158
|
+
once: input.options?.once
|
|
159
|
+
});
|
|
160
|
+
return { success: true, listening: input.event };
|
|
161
|
+
|
|
162
|
+
case 'remove':
|
|
163
|
+
return { success: true, removed: input.event };
|
|
164
|
+
|
|
165
|
+
default:
|
|
166
|
+
throw new Error('Unknown event action');
|
|
167
|
+
}
|
|
168
|
+
}),
|
|
169
|
+
|
|
170
|
+
formControl: createAITool('client-forms', {
|
|
171
|
+
permissions: {
|
|
172
|
+
allowClientExecution: true
|
|
173
|
+
},
|
|
174
|
+
audit: true
|
|
175
|
+
})
|
|
176
|
+
.input(z.object({
|
|
177
|
+
action: z.enum(['fill', 'submit', 'reset', 'validate', 'getData']),
|
|
178
|
+
selector: z.string(),
|
|
179
|
+
data: z.record(z.any()).optional(),
|
|
180
|
+
validateRules: z.record(z.any()).optional()
|
|
181
|
+
}))
|
|
182
|
+
.describe('Control and interact with forms')
|
|
183
|
+
.tag('client', 'forms', 'input')
|
|
184
|
+
.clientExecute(async ({ input }) => {
|
|
185
|
+
const form = document.querySelector(input.selector) as HTMLFormElement;
|
|
186
|
+
if (!form) throw new Error(`Form not found: ${input.selector}`);
|
|
187
|
+
|
|
188
|
+
switch (input.action) {
|
|
189
|
+
case 'fill':
|
|
190
|
+
if (!input.data) throw new Error('Data required for fill action');
|
|
191
|
+
Object.entries(input.data).forEach(([name, value]) => {
|
|
192
|
+
const field = form.elements.namedItem(name) as HTMLInputElement;
|
|
193
|
+
if (field) {
|
|
194
|
+
field.value = String(value);
|
|
195
|
+
field.dispatchEvent(new Event('input', { bubbles: true }));
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
return { success: true, filled: Object.keys(input.data).length };
|
|
199
|
+
|
|
200
|
+
case 'submit':
|
|
201
|
+
const submitEvent = new Event('submit', {
|
|
202
|
+
bubbles: true,
|
|
203
|
+
cancelable: true
|
|
204
|
+
});
|
|
205
|
+
const submitted = form.dispatchEvent(submitEvent);
|
|
206
|
+
if (submitted) form.submit();
|
|
207
|
+
return { success: submitted, submitted: true };
|
|
208
|
+
|
|
209
|
+
case 'reset':
|
|
210
|
+
form.reset();
|
|
211
|
+
return { success: true, reset: true };
|
|
212
|
+
|
|
213
|
+
case 'validate':
|
|
214
|
+
const isValid = form.checkValidity();
|
|
215
|
+
const invalidFields = Array.from(form.elements)
|
|
216
|
+
.filter(el => el instanceof HTMLInputElement && !el.checkValidity())
|
|
217
|
+
.map(el => (el as HTMLInputElement).name);
|
|
218
|
+
return {
|
|
219
|
+
valid: isValid,
|
|
220
|
+
invalidFields
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
case 'getData':
|
|
224
|
+
const formData = new FormData(form);
|
|
225
|
+
const data = Object.fromEntries(formData.entries());
|
|
226
|
+
return { data, fields: Object.keys(data).length };
|
|
227
|
+
|
|
228
|
+
default:
|
|
229
|
+
throw new Error('Unknown form action');
|
|
230
|
+
}
|
|
231
|
+
}),
|
|
232
|
+
|
|
233
|
+
storageControl: createAITool('client-storage', {
|
|
234
|
+
permissions: {
|
|
235
|
+
allowClientExecution: true
|
|
236
|
+
},
|
|
237
|
+
audit: true
|
|
238
|
+
})
|
|
239
|
+
.input(z.object({
|
|
240
|
+
type: z.enum(['local', 'session', 'cookie']),
|
|
241
|
+
operation: z.enum(['get', 'set', 'remove', 'clear', 'list']),
|
|
242
|
+
key: z.string().optional(),
|
|
243
|
+
value: z.any().optional(),
|
|
244
|
+
options: z.object({
|
|
245
|
+
expires: z.number().optional(),
|
|
246
|
+
path: z.string().optional(),
|
|
247
|
+
domain: z.string().optional(),
|
|
248
|
+
secure: z.boolean().optional(),
|
|
249
|
+
sameSite: z.enum(['strict', 'lax', 'none']).optional()
|
|
250
|
+
}).optional()
|
|
251
|
+
}))
|
|
252
|
+
.describe('Manage client-side storage')
|
|
253
|
+
.tag('client', 'storage', 'persistence')
|
|
254
|
+
.clientExecute(async ({ input }) => {
|
|
255
|
+
const storage = input.type === 'local' ? localStorage :
|
|
256
|
+
input.type === 'session' ? sessionStorage : null;
|
|
257
|
+
|
|
258
|
+
if (input.type === 'cookie') {
|
|
259
|
+
switch (input.operation) {
|
|
260
|
+
case 'set':
|
|
261
|
+
if (!input.key || input.value === undefined) {
|
|
262
|
+
throw new Error('Key and value required for cookie set');
|
|
263
|
+
}
|
|
264
|
+
let cookieStr = `${input.key}=${JSON.stringify(input.value)}`;
|
|
265
|
+
if (input.options?.expires) {
|
|
266
|
+
const date = new Date();
|
|
267
|
+
date.setTime(date.getTime() + input.options.expires);
|
|
268
|
+
cookieStr += `; expires=${date.toUTCString()}`;
|
|
269
|
+
}
|
|
270
|
+
if (input.options?.path) cookieStr += `; path=${input.options.path}`;
|
|
271
|
+
if (input.options?.domain) cookieStr += `; domain=${input.options.domain}`;
|
|
272
|
+
if (input.options?.secure) cookieStr += '; secure';
|
|
273
|
+
if (input.options?.sameSite) cookieStr += `; samesite=${input.options.sameSite}`;
|
|
274
|
+
document.cookie = cookieStr;
|
|
275
|
+
return { success: true, key: input.key };
|
|
276
|
+
|
|
277
|
+
case 'get':
|
|
278
|
+
if (!input.key) throw new Error('Key required for cookie get');
|
|
279
|
+
const cookies = document.cookie.split(';');
|
|
280
|
+
const foundCookie = cookies.find(c => c.trim().startsWith(`${input.key}=`));
|
|
281
|
+
const value = foundCookie ? JSON.parse(foundCookie.split('=')[1]) : null;
|
|
282
|
+
return { value, key: input.key };
|
|
283
|
+
|
|
284
|
+
case 'remove':
|
|
285
|
+
if (!input.key) throw new Error('Key required for cookie remove');
|
|
286
|
+
document.cookie = `${input.key}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
|
|
287
|
+
return { success: true, removed: input.key };
|
|
288
|
+
|
|
289
|
+
case 'list':
|
|
290
|
+
const allCookies = document.cookie.split(';').map(c => {
|
|
291
|
+
const [key] = c.trim().split('=');
|
|
292
|
+
return key;
|
|
293
|
+
}).filter(Boolean);
|
|
294
|
+
return { keys: allCookies, count: allCookies.length };
|
|
295
|
+
|
|
296
|
+
case 'clear':
|
|
297
|
+
document.cookie.split(';').forEach(c => {
|
|
298
|
+
const [key] = c.trim().split('=');
|
|
299
|
+
if (key) {
|
|
300
|
+
document.cookie = `${key}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
return { success: true, cleared: true };
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (!storage) throw new Error('Storage type not supported');
|
|
308
|
+
|
|
309
|
+
switch (input.operation) {
|
|
310
|
+
case 'get':
|
|
311
|
+
if (!input.key) throw new Error('Key required for get operation');
|
|
312
|
+
const value = storage.getItem(input.key);
|
|
313
|
+
return {
|
|
314
|
+
value: value ? JSON.parse(value) : null,
|
|
315
|
+
key: input.key
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
case 'set':
|
|
319
|
+
if (!input.key || input.value === undefined) {
|
|
320
|
+
throw new Error('Key and value required for set operation');
|
|
321
|
+
}
|
|
322
|
+
storage.setItem(input.key, JSON.stringify(input.value));
|
|
323
|
+
return { success: true, key: input.key };
|
|
324
|
+
|
|
325
|
+
case 'remove':
|
|
326
|
+
if (!input.key) throw new Error('Key required for remove operation');
|
|
327
|
+
storage.removeItem(input.key);
|
|
328
|
+
return { success: true, removed: input.key };
|
|
329
|
+
|
|
330
|
+
case 'clear':
|
|
331
|
+
storage.clear();
|
|
332
|
+
return { success: true, cleared: true };
|
|
333
|
+
|
|
334
|
+
case 'list':
|
|
335
|
+
const keys = Object.keys(storage);
|
|
336
|
+
return { keys, count: keys.length };
|
|
337
|
+
|
|
338
|
+
default:
|
|
339
|
+
throw new Error('Unknown storage operation');
|
|
340
|
+
}
|
|
341
|
+
}),
|
|
342
|
+
|
|
343
|
+
navigationControl: createAITool('client-navigation', {
|
|
344
|
+
permissions: {
|
|
345
|
+
allowClientExecution: true
|
|
346
|
+
},
|
|
347
|
+
audit: true
|
|
348
|
+
})
|
|
349
|
+
.input(z.object({
|
|
350
|
+
action: z.enum(['navigate', 'back', 'forward', 'reload', 'getLocation', 'setHash']),
|
|
351
|
+
url: z.string().optional(),
|
|
352
|
+
replace: z.boolean().optional(),
|
|
353
|
+
state: z.any().optional()
|
|
354
|
+
}))
|
|
355
|
+
.describe('Control browser navigation')
|
|
356
|
+
.tag('client', 'navigation', 'routing')
|
|
357
|
+
.clientExecute(async ({ input }) => {
|
|
358
|
+
switch (input.action) {
|
|
359
|
+
case 'navigate':
|
|
360
|
+
if (!input.url) throw new Error('URL required for navigation');
|
|
361
|
+
if (input.replace) {
|
|
362
|
+
window.location.replace(input.url);
|
|
363
|
+
} else {
|
|
364
|
+
window.history.pushState(input.state || {}, '', input.url);
|
|
365
|
+
}
|
|
366
|
+
return { success: true, navigated: input.url };
|
|
367
|
+
|
|
368
|
+
case 'back':
|
|
369
|
+
window.history.back();
|
|
370
|
+
return { success: true, action: 'back' };
|
|
371
|
+
|
|
372
|
+
case 'forward':
|
|
373
|
+
window.history.forward();
|
|
374
|
+
return { success: true, action: 'forward' };
|
|
375
|
+
|
|
376
|
+
case 'reload':
|
|
377
|
+
window.location.reload();
|
|
378
|
+
return { success: true, action: 'reload' };
|
|
379
|
+
|
|
380
|
+
case 'getLocation':
|
|
381
|
+
return {
|
|
382
|
+
href: window.location.href,
|
|
383
|
+
pathname: window.location.pathname,
|
|
384
|
+
search: window.location.search,
|
|
385
|
+
hash: window.location.hash,
|
|
386
|
+
host: window.location.host,
|
|
387
|
+
hostname: window.location.hostname,
|
|
388
|
+
port: window.location.port,
|
|
389
|
+
protocol: window.location.protocol
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
case 'setHash':
|
|
393
|
+
if (input.url) window.location.hash = input.url;
|
|
394
|
+
return { success: true, hash: window.location.hash };
|
|
395
|
+
|
|
396
|
+
default:
|
|
397
|
+
throw new Error('Unknown navigation action');
|
|
398
|
+
}
|
|
399
|
+
}),
|
|
400
|
+
|
|
401
|
+
animationControl: createAITool('client-animation', {
|
|
402
|
+
permissions: {
|
|
403
|
+
allowClientExecution: true
|
|
404
|
+
},
|
|
405
|
+
audit: true
|
|
406
|
+
})
|
|
407
|
+
.input(z.object({
|
|
408
|
+
selector: z.string(),
|
|
409
|
+
animation: z.object({
|
|
410
|
+
keyframes: z.array(z.record(z.any())),
|
|
411
|
+
options: z.object({
|
|
412
|
+
duration: z.number().optional(),
|
|
413
|
+
easing: z.string().optional(),
|
|
414
|
+
delay: z.number().optional(),
|
|
415
|
+
iterations: z.number().optional(),
|
|
416
|
+
direction: z.enum(['normal', 'reverse', 'alternate', 'alternate-reverse']).optional(),
|
|
417
|
+
fill: z.enum(['none', 'forwards', 'backwards', 'both']).optional()
|
|
418
|
+
}).optional()
|
|
419
|
+
})
|
|
420
|
+
}))
|
|
421
|
+
.describe('Create and control animations')
|
|
422
|
+
.tag('client', 'animation', 'visual')
|
|
423
|
+
.clientExecute(async ({ input }) => {
|
|
424
|
+
const elements = document.querySelectorAll(input.selector);
|
|
425
|
+
if (elements.length === 0) {
|
|
426
|
+
throw new Error(`No elements found for selector: ${input.selector}`);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const animations: Animation[] = [];
|
|
430
|
+
elements.forEach(el => {
|
|
431
|
+
const animation = el.animate(
|
|
432
|
+
input.animation.keyframes,
|
|
433
|
+
input.animation.options || { duration: 1000 }
|
|
434
|
+
);
|
|
435
|
+
animations.push(animation);
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
return {
|
|
439
|
+
success: true,
|
|
440
|
+
animated: elements.length,
|
|
441
|
+
duration: input.animation.options?.duration || 1000
|
|
442
|
+
};
|
|
443
|
+
}),
|
|
444
|
+
|
|
445
|
+
mediaControl: createAITool('client-media', {
|
|
446
|
+
permissions: {
|
|
447
|
+
allowClientExecution: true
|
|
448
|
+
},
|
|
449
|
+
audit: true
|
|
450
|
+
})
|
|
451
|
+
.input(z.object({
|
|
452
|
+
selector: z.string(),
|
|
453
|
+
action: z.enum(['play', 'pause', 'stop', 'mute', 'unmute', 'setVolume', 'seek']),
|
|
454
|
+
value: z.number().optional()
|
|
455
|
+
}))
|
|
456
|
+
.describe('Control media elements')
|
|
457
|
+
.tag('client', 'media', 'audio', 'video')
|
|
458
|
+
.clientExecute(async ({ input }) => {
|
|
459
|
+
const media = document.querySelector(input.selector) as HTMLMediaElement;
|
|
460
|
+
if (!media) throw new Error(`Media element not found: ${input.selector}`);
|
|
461
|
+
|
|
462
|
+
switch (input.action) {
|
|
463
|
+
case 'play':
|
|
464
|
+
await media.play();
|
|
465
|
+
return { success: true, playing: true };
|
|
466
|
+
|
|
467
|
+
case 'pause':
|
|
468
|
+
media.pause();
|
|
469
|
+
return { success: true, paused: true };
|
|
470
|
+
|
|
471
|
+
case 'stop':
|
|
472
|
+
media.pause();
|
|
473
|
+
media.currentTime = 0;
|
|
474
|
+
return { success: true, stopped: true };
|
|
475
|
+
|
|
476
|
+
case 'mute':
|
|
477
|
+
media.muted = true;
|
|
478
|
+
return { success: true, muted: true };
|
|
479
|
+
|
|
480
|
+
case 'unmute':
|
|
481
|
+
media.muted = false;
|
|
482
|
+
return { success: true, muted: false };
|
|
483
|
+
|
|
484
|
+
case 'setVolume':
|
|
485
|
+
if (input.value === undefined) throw new Error('Value required for setVolume');
|
|
486
|
+
media.volume = Math.max(0, Math.min(1, input.value));
|
|
487
|
+
return { success: true, volume: media.volume };
|
|
488
|
+
|
|
489
|
+
case 'seek':
|
|
490
|
+
if (input.value === undefined) throw new Error('Value required for seek');
|
|
491
|
+
media.currentTime = input.value;
|
|
492
|
+
return { success: true, currentTime: media.currentTime };
|
|
493
|
+
|
|
494
|
+
default:
|
|
495
|
+
throw new Error('Unknown media action');
|
|
496
|
+
}
|
|
497
|
+
})
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
export function createClientControlSystem() {
|
|
501
|
+
const tools = new Map<string, AIControlledTool>();
|
|
502
|
+
|
|
503
|
+
Object.values(clientTools).forEach(tool => {
|
|
504
|
+
tools.set(tool.name, tool);
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
return {
|
|
508
|
+
tools,
|
|
509
|
+
|
|
510
|
+
async executeClientTool(
|
|
511
|
+
name: string,
|
|
512
|
+
input: any,
|
|
513
|
+
context?: ClientControlContext
|
|
514
|
+
) {
|
|
515
|
+
const tool = tools.get(name);
|
|
516
|
+
if (!tool) {
|
|
517
|
+
throw new Error(`Client tool "${name}" not found`);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const clientContext: ClientControlContext = {
|
|
521
|
+
...context,
|
|
522
|
+
isServer: false,
|
|
523
|
+
cache: context?.cache || new Map(),
|
|
524
|
+
fetch: fetch,
|
|
525
|
+
window: typeof window !== 'undefined' ? window : undefined,
|
|
526
|
+
document: typeof document !== 'undefined' ? document : undefined,
|
|
527
|
+
navigator: typeof navigator !== 'undefined' ? navigator : undefined,
|
|
528
|
+
localStorage: typeof localStorage !== 'undefined' ? localStorage : undefined,
|
|
529
|
+
sessionStorage: typeof sessionStorage !== 'undefined' ? sessionStorage : undefined
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
return await tool.run(input, clientContext);
|
|
533
|
+
},
|
|
534
|
+
|
|
535
|
+
registerClientTool(tool: AIControlledTool) {
|
|
536
|
+
tools.set(tool.name, tool);
|
|
537
|
+
return this;
|
|
538
|
+
},
|
|
539
|
+
|
|
540
|
+
listClientTools() {
|
|
541
|
+
return Array.from(tools.values()).map(tool => ({
|
|
542
|
+
name: tool.name,
|
|
543
|
+
description: tool.description,
|
|
544
|
+
tags: tool.tags,
|
|
545
|
+
schema: tool.schema
|
|
546
|
+
}));
|
|
547
|
+
}
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
export const clientControlSystem = createClientControlSystem();
|