@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.
Files changed (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +190 -0
  3. package/lib/aui/README.md +136 -0
  4. package/lib/aui/__tests__/aui-complete.test.ts +251 -0
  5. package/lib/aui/__tests__/aui-comprehensive.test.ts +376 -0
  6. package/lib/aui/__tests__/aui-concise.test.ts +278 -0
  7. package/lib/aui/__tests__/aui-integration.test.ts +309 -0
  8. package/lib/aui/__tests__/aui-simple.test.ts +116 -0
  9. package/lib/aui/__tests__/aui.test.ts +269 -0
  10. package/lib/aui/__tests__/concise-api.test.ts +165 -0
  11. package/lib/aui/__tests__/core.test.ts +265 -0
  12. package/lib/aui/__tests__/simple-api.test.ts +200 -0
  13. package/lib/aui/ai-assistant.ts +408 -0
  14. package/lib/aui/ai-control.ts +353 -0
  15. package/lib/aui/client/use-aui.ts +55 -0
  16. package/lib/aui/client-control.ts +551 -0
  17. package/lib/aui/client-executor.ts +417 -0
  18. package/lib/aui/components/ToolRenderer.tsx +22 -0
  19. package/lib/aui/core.ts +137 -0
  20. package/lib/aui/demo.tsx +89 -0
  21. package/lib/aui/examples/ai-complete-demo.tsx +359 -0
  22. package/lib/aui/examples/ai-control-demo.tsx +356 -0
  23. package/lib/aui/examples/ai-control-tools.ts +308 -0
  24. package/lib/aui/examples/concise-api.tsx +153 -0
  25. package/lib/aui/examples/index.tsx +163 -0
  26. package/lib/aui/examples/quick-demo.tsx +91 -0
  27. package/lib/aui/examples/simple-demo.tsx +71 -0
  28. package/lib/aui/examples/simple-tools.tsx +160 -0
  29. package/lib/aui/examples/user-api.tsx +208 -0
  30. package/lib/aui/examples/user-requested.tsx +174 -0
  31. package/lib/aui/examples/weather-search-tools.tsx +119 -0
  32. package/lib/aui/examples.tsx +367 -0
  33. package/lib/aui/hooks/useAUITool.ts +142 -0
  34. package/lib/aui/hooks/useAUIToolEnhanced.ts +343 -0
  35. package/lib/aui/hooks/useAUITools.ts +195 -0
  36. package/lib/aui/index.ts +156 -0
  37. package/lib/aui/provider.tsx +45 -0
  38. package/lib/aui/server-control.ts +386 -0
  39. package/lib/aui/server-executor.ts +165 -0
  40. package/lib/aui/server.ts +167 -0
  41. package/lib/aui/tool-registry.ts +380 -0
  42. package/lib/aui/tools/advanced-examples.tsx +86 -0
  43. package/lib/aui/tools/ai-complete.ts +375 -0
  44. package/lib/aui/tools/api-tools.tsx +230 -0
  45. package/lib/aui/tools/data-tools.tsx +232 -0
  46. package/lib/aui/tools/dom-tools.tsx +202 -0
  47. package/lib/aui/tools/examples.ts +43 -0
  48. package/lib/aui/tools/file-tools.tsx +202 -0
  49. package/lib/aui/tools/form-tools.tsx +233 -0
  50. package/lib/aui/tools/index.ts +8 -0
  51. package/lib/aui/tools/navigation-tools.tsx +172 -0
  52. package/lib/aui/tools/notification-tools.ts +213 -0
  53. package/lib/aui/tools/state-tools.tsx +209 -0
  54. package/lib/aui/types.ts +47 -0
  55. package/lib/aui/vercel-ai.ts +100 -0
  56. package/package.json +51 -0
@@ -0,0 +1,232 @@
1
+ import { z } from 'zod';
2
+ import { createAITool } from '../ai-control';
3
+
4
+ export const dataFetch = createAITool('data.fetch')
5
+ .describe('Fetch and process data from various sources')
6
+ .tag('data', 'fetch', 'async')
7
+ .input(z.object({
8
+ source: z.string(),
9
+ transform: z.enum(['json', 'text', 'csv', 'xml']).optional(),
10
+ cache: z.boolean().optional()
11
+ }))
12
+ .execute(async ({ input, ctx }) => {
13
+ const cacheKey = `data_${input.source}`;
14
+
15
+ if (input.cache && ctx?.cache.has(cacheKey)) {
16
+ return ctx.cache.get(cacheKey);
17
+ }
18
+
19
+ const response = await ctx!.fetch(input.source);
20
+ let data: any;
21
+
22
+ switch (input.transform) {
23
+ case 'json':
24
+ data = await response.json();
25
+ break;
26
+ case 'csv':
27
+ const text = await response.text();
28
+ data = text.split('\n').map(line => line.split(','));
29
+ break;
30
+ case 'xml':
31
+ data = await response.text();
32
+ break;
33
+ default:
34
+ data = await response.text();
35
+ }
36
+
37
+ if (input.cache) {
38
+ ctx?.cache.set(cacheKey, data);
39
+ }
40
+
41
+ return { data, source: input.source, cached: false };
42
+ });
43
+
44
+ export const dataTransform = createAITool('data.transform')
45
+ .describe('Transform data using various operations')
46
+ .tag('data', 'transform', 'utility')
47
+ .input(z.object({
48
+ data: z.any(),
49
+ operation: z.enum(['filter', 'map', 'reduce', 'sort', 'group']),
50
+ params: z.any()
51
+ }))
52
+ .execute(async ({ input }) => {
53
+ let result: any;
54
+
55
+ switch (input.operation) {
56
+ case 'filter':
57
+ if (Array.isArray(input.data)) {
58
+ result = input.data.filter((item: any) => {
59
+ return Object.entries(input.params).every(([key, value]) => item[key] === value);
60
+ });
61
+ }
62
+ break;
63
+
64
+ case 'map':
65
+ if (Array.isArray(input.data)) {
66
+ result = input.data.map((item: any) => {
67
+ const mapped: any = {};
68
+ Object.entries(input.params).forEach(([newKey, oldKey]) => {
69
+ mapped[newKey] = item[oldKey as string];
70
+ });
71
+ return mapped;
72
+ });
73
+ }
74
+ break;
75
+
76
+ case 'sort':
77
+ if (Array.isArray(input.data)) {
78
+ const { field, order = 'asc' } = input.params;
79
+ result = [...input.data].sort((a, b) => {
80
+ const aVal = a[field];
81
+ const bVal = b[field];
82
+ return order === 'asc' ? (aVal > bVal ? 1 : -1) : (aVal < bVal ? 1 : -1);
83
+ });
84
+ }
85
+ break;
86
+
87
+ case 'group':
88
+ if (Array.isArray(input.data)) {
89
+ const { by } = input.params;
90
+ result = input.data.reduce((acc: any, item: any) => {
91
+ const key = item[by];
92
+ if (!acc[key]) acc[key] = [];
93
+ acc[key].push(item);
94
+ return acc;
95
+ }, {});
96
+ }
97
+ break;
98
+
99
+ default:
100
+ result = input.data;
101
+ }
102
+
103
+ return { result, operation: input.operation };
104
+ });
105
+
106
+ export const dataValidate = createAITool('data.validate')
107
+ .describe('Validate data against a schema')
108
+ .tag('data', 'validation', 'utility')
109
+ .input(z.object({
110
+ data: z.any(),
111
+ schema: z.any(),
112
+ strict: z.boolean().optional()
113
+ }))
114
+ .execute(async ({ input }) => {
115
+ try {
116
+ const validated = input.schema.parse(input.data);
117
+ return { valid: true, data: validated, errors: [] };
118
+ } catch (error: any) {
119
+ return {
120
+ valid: false,
121
+ data: input.data,
122
+ errors: error.errors || [{ message: error.message }]
123
+ };
124
+ }
125
+ });
126
+
127
+ export const dataAggregate = createAITool('data.aggregate')
128
+ .describe('Perform aggregation operations on data')
129
+ .tag('data', 'analytics', 'utility')
130
+ .input(z.object({
131
+ data: z.array(z.any()),
132
+ operation: z.enum(['sum', 'avg', 'min', 'max', 'count', 'distinct']),
133
+ field: z.string().optional()
134
+ }))
135
+ .execute(async ({ input }) => {
136
+ let result: any;
137
+
138
+ switch (input.operation) {
139
+ case 'sum':
140
+ result = input.data.reduce((sum, item) =>
141
+ sum + (input.field ? item[input.field] : item), 0);
142
+ break;
143
+
144
+ case 'avg':
145
+ const sum = input.data.reduce((s, item) =>
146
+ s + (input.field ? item[input.field] : item), 0);
147
+ result = sum / input.data.length;
148
+ break;
149
+
150
+ case 'min':
151
+ result = Math.min(...input.data.map(item =>
152
+ input.field ? item[input.field] : item));
153
+ break;
154
+
155
+ case 'max':
156
+ result = Math.max(...input.data.map(item =>
157
+ input.field ? item[input.field] : item));
158
+ break;
159
+
160
+ case 'count':
161
+ result = input.data.length;
162
+ break;
163
+
164
+ case 'distinct':
165
+ const values = input.data.map(item =>
166
+ input.field ? item[input.field] : item);
167
+ result = [...new Set(values)];
168
+ break;
169
+ }
170
+
171
+ return { result, operation: input.operation, field: input.field };
172
+ });
173
+
174
+ export const dataPaginate = createAITool('data.paginate')
175
+ .describe('Paginate data')
176
+ .tag('data', 'pagination', 'utility')
177
+ .input(z.object({
178
+ data: z.array(z.any()),
179
+ page: z.number().min(1),
180
+ pageSize: z.number().min(1),
181
+ includeMetadata: z.boolean().optional()
182
+ }))
183
+ .execute(async ({ input }) => {
184
+ const start = (input.page - 1) * input.pageSize;
185
+ const end = start + input.pageSize;
186
+ const items = input.data.slice(start, end);
187
+ const totalPages = Math.ceil(input.data.length / input.pageSize);
188
+
189
+ const result: any = { items };
190
+
191
+ if (input.includeMetadata) {
192
+ result.metadata = {
193
+ page: input.page,
194
+ pageSize: input.pageSize,
195
+ total: input.data.length,
196
+ totalPages,
197
+ hasNext: input.page < totalPages,
198
+ hasPrev: input.page > 1
199
+ };
200
+ }
201
+
202
+ return result;
203
+ });
204
+
205
+ export const dataSearch = createAITool('data.search')
206
+ .describe('Search through data')
207
+ .tag('data', 'search', 'utility')
208
+ .input(z.object({
209
+ data: z.array(z.any()),
210
+ query: z.string(),
211
+ fields: z.array(z.string()).optional(),
212
+ fuzzy: z.boolean().optional()
213
+ }))
214
+ .execute(async ({ input }) => {
215
+ const query = input.query.toLowerCase();
216
+
217
+ const results = input.data.filter(item => {
218
+ const fieldsToSearch = input.fields || Object.keys(item);
219
+
220
+ return fieldsToSearch.some(field => {
221
+ const value = String(item[field]).toLowerCase();
222
+
223
+ if (input.fuzzy) {
224
+ return value.includes(query) || query.includes(value);
225
+ } else {
226
+ return value === query;
227
+ }
228
+ });
229
+ });
230
+
231
+ return { results, count: results.length, query: input.query };
232
+ });
@@ -0,0 +1,202 @@
1
+ import React from 'react';
2
+ import { z } from 'zod';
3
+ import { createAITool } from '../ai-control';
4
+
5
+ export const domClick = createAITool('dom.click')
6
+ .describe('Click on a DOM element')
7
+ .tag('dom', 'interaction', 'client')
8
+ .input(z.object({
9
+ selector: z.string().describe('CSS selector for the element'),
10
+ wait: z.number().optional().describe('Wait time in ms before clicking')
11
+ }))
12
+ .clientExecute(async ({ input }) => {
13
+ if (input.wait) {
14
+ await new Promise(resolve => setTimeout(resolve, input.wait));
15
+ }
16
+ const element = document.querySelector(input.selector) as HTMLElement;
17
+ if (!element) throw new Error(`Element not found: ${input.selector}`);
18
+ element.click();
19
+ return { success: true, selector: input.selector, timestamp: Date.now() };
20
+ })
21
+ .render(({ data }) => (
22
+ <span className="text-green-600">✓ Clicked {data.selector}</span>
23
+ ));
24
+
25
+ export const domType = createAITool('dom.type')
26
+ .describe('Type text into an input element')
27
+ .tag('dom', 'input', 'client')
28
+ .input(z.object({
29
+ selector: z.string(),
30
+ text: z.string(),
31
+ clear: z.boolean().optional().describe('Clear field before typing'),
32
+ delay: z.number().optional().describe('Delay between keystrokes in ms')
33
+ }))
34
+ .clientExecute(async ({ input }) => {
35
+ const element = document.querySelector(input.selector) as HTMLInputElement;
36
+ if (!element) throw new Error(`Element not found: ${input.selector}`);
37
+
38
+ if (input.clear) {
39
+ element.value = '';
40
+ }
41
+
42
+ if (input.delay) {
43
+ for (const char of input.text) {
44
+ element.value += char;
45
+ element.dispatchEvent(new Event('input', { bubbles: true }));
46
+ await new Promise(resolve => setTimeout(resolve, input.delay));
47
+ }
48
+ } else {
49
+ element.value = input.clear ? input.text : element.value + input.text;
50
+ element.dispatchEvent(new Event('input', { bubbles: true }));
51
+ }
52
+
53
+ element.dispatchEvent(new Event('change', { bubbles: true }));
54
+ return { success: true, selector: input.selector, text: input.text };
55
+ })
56
+ .render(({ data }) => (
57
+ <span className="text-blue-600">✓ Typed &quot;{data.text}&quot; into {data.selector}</span>
58
+ ));
59
+
60
+ export const domSelect = createAITool('dom.select')
61
+ .describe('Select an option from a dropdown')
62
+ .tag('dom', 'input', 'client')
63
+ .input(z.object({
64
+ selector: z.string(),
65
+ value: z.string().optional(),
66
+ index: z.number().optional(),
67
+ text: z.string().optional()
68
+ }))
69
+ .clientExecute(async ({ input }) => {
70
+ const element = document.querySelector(input.selector) as HTMLSelectElement;
71
+ if (!element) throw new Error(`Element not found: ${input.selector}`);
72
+
73
+ if (input.value !== undefined) {
74
+ element.value = input.value;
75
+ } else if (input.index !== undefined) {
76
+ element.selectedIndex = input.index;
77
+ } else if (input.text) {
78
+ const option = Array.from(element.options).find(opt => opt.text === input.text);
79
+ if (option) element.value = option.value;
80
+ }
81
+
82
+ element.dispatchEvent(new Event('change', { bubbles: true }));
83
+ return { success: true, selector: input.selector, value: element.value };
84
+ });
85
+
86
+ export const domScroll = createAITool('dom.scroll')
87
+ .describe('Scroll to an element or position')
88
+ .tag('dom', 'navigation', 'client')
89
+ .input(z.object({
90
+ selector: z.string().optional(),
91
+ x: z.number().optional(),
92
+ y: z.number().optional(),
93
+ behavior: z.enum(['auto', 'smooth']).optional()
94
+ }))
95
+ .clientExecute(async ({ input }) => {
96
+ if (input.selector) {
97
+ const element = document.querySelector(input.selector);
98
+ if (!element) throw new Error(`Element not found: ${input.selector}`);
99
+ element.scrollIntoView({ behavior: input.behavior || 'smooth' });
100
+ } else {
101
+ window.scrollTo({
102
+ left: input.x || 0,
103
+ top: input.y || 0,
104
+ behavior: input.behavior || 'smooth'
105
+ });
106
+ }
107
+ return { success: true, scrolled: true };
108
+ });
109
+
110
+ export const domWaitFor = createAITool('dom.waitFor')
111
+ .describe('Wait for an element to appear')
112
+ .tag('dom', 'utility', 'client')
113
+ .input(z.object({
114
+ selector: z.string(),
115
+ timeout: z.number().optional().default(5000),
116
+ interval: z.number().optional().default(100)
117
+ }))
118
+ .clientExecute(async ({ input }) => {
119
+ const startTime = Date.now();
120
+
121
+ const timeout = input.timeout || 5000;
122
+ const interval = input.interval || 100;
123
+
124
+ while (Date.now() - startTime < timeout) {
125
+ const element = document.querySelector(input.selector);
126
+ if (element) {
127
+ return { success: true, found: true, selector: input.selector };
128
+ }
129
+ await new Promise(resolve => setTimeout(resolve, interval));
130
+ }
131
+
132
+ throw new Error(`Element ${input.selector} not found after ${timeout}ms`);
133
+ });
134
+
135
+ export const domGetText = createAITool('dom.getText')
136
+ .describe('Get text content from elements')
137
+ .tag('dom', 'read', 'client')
138
+ .input(z.object({
139
+ selector: z.string(),
140
+ all: z.boolean().optional()
141
+ }))
142
+ .clientExecute(async ({ input }) => {
143
+ if (input.all) {
144
+ const elements = document.querySelectorAll(input.selector);
145
+ const texts = Array.from(elements).map(el => el.textContent || '');
146
+ return { texts, count: texts.length };
147
+ } else {
148
+ const element = document.querySelector(input.selector);
149
+ if (!element) throw new Error(`Element not found: ${input.selector}`);
150
+ return { text: element.textContent || '' };
151
+ }
152
+ });
153
+
154
+ export const domGetAttribute = createAITool('dom.getAttribute')
155
+ .describe('Get attribute value from an element')
156
+ .tag('dom', 'read', 'client')
157
+ .input(z.object({
158
+ selector: z.string(),
159
+ attribute: z.string()
160
+ }))
161
+ .clientExecute(async ({ input }) => {
162
+ const element = document.querySelector(input.selector);
163
+ if (!element) throw new Error(`Element not found: ${input.selector}`);
164
+ return { value: element.getAttribute(input.attribute) };
165
+ });
166
+
167
+ export const domSetAttribute = createAITool('dom.setAttribute')
168
+ .describe('Set attribute value on an element')
169
+ .tag('dom', 'modify', 'client')
170
+ .input(z.object({
171
+ selector: z.string(),
172
+ attribute: z.string(),
173
+ value: z.string()
174
+ }))
175
+ .clientExecute(async ({ input }) => {
176
+ const element = document.querySelector(input.selector);
177
+ if (!element) throw new Error(`Element not found: ${input.selector}`);
178
+ element.setAttribute(input.attribute, input.value);
179
+ return { success: true, selector: input.selector, attribute: input.attribute };
180
+ });
181
+
182
+ export const domToggleClass = createAITool('dom.toggleClass')
183
+ .describe('Toggle CSS class on elements')
184
+ .tag('dom', 'style', 'client')
185
+ .input(z.object({
186
+ selector: z.string(),
187
+ className: z.string(),
188
+ action: z.enum(['add', 'remove', 'toggle']).optional()
189
+ }))
190
+ .clientExecute(async ({ input }) => {
191
+ const elements = document.querySelectorAll(input.selector);
192
+ elements.forEach(element => {
193
+ if (input.action === 'add') {
194
+ element.classList.add(input.className);
195
+ } else if (input.action === 'remove') {
196
+ element.classList.remove(input.className);
197
+ } else {
198
+ element.classList.toggle(input.className);
199
+ }
200
+ });
201
+ return { success: true, affected: elements.length };
202
+ });
@@ -0,0 +1,43 @@
1
+ import aui, { z } from '../index';
2
+
3
+ export const weatherTool = aui
4
+ .tool('weather')
5
+ .input(z.object({ city: z.string() }))
6
+ .execute(async ({ input }) => ({
7
+ city: input.city,
8
+ temperature: Math.floor(Math.random() * 30 + 50),
9
+ condition: ['Sunny', 'Cloudy', 'Rainy'][Math.floor(Math.random() * 3)]
10
+ }));
11
+
12
+ export const searchTool = aui
13
+ .tool('search')
14
+ .input(z.object({ query: z.string() }))
15
+ .execute(async ({ input }) => {
16
+ await new Promise(resolve => setTimeout(resolve, 500));
17
+ return Array.from({ length: 5 }, (_, i) => ({
18
+ id: i + 1,
19
+ title: `Result ${i + 1} for "${input.query}"`,
20
+ url: `https://example.com/${i + 1}`
21
+ }));
22
+ });
23
+
24
+ export const calculatorTool = aui
25
+ .tool('calculator')
26
+ .input(z.object({ expression: z.string() }))
27
+ .execute(async ({ input }) => {
28
+ try {
29
+ // Simple safe evaluation for demo
30
+ const result = Function('"use strict"; return (' + input.expression + ')')();
31
+ return { expression: input.expression, result };
32
+ } catch {
33
+ throw new Error('Invalid expression');
34
+ }
35
+ });
36
+
37
+ export const dataFetcherTool = aui
38
+ .tool('dataFetcher')
39
+ .input(z.object({ url: z.string().url() }))
40
+ .execute(async ({ input }) => {
41
+ const response = await fetch(input.url);
42
+ return response.json();
43
+ });
@@ -0,0 +1,202 @@
1
+ import { z } from 'zod';
2
+ import { createAITool } from '../ai-control';
3
+
4
+ export const fileUpload = createAITool('file.upload')
5
+ .describe('Handle file uploads')
6
+ .tag('file', 'upload', 'client')
7
+ .input(z.object({
8
+ accept: z.string().optional(),
9
+ multiple: z.boolean().optional(),
10
+ maxSize: z.number().optional()
11
+ }))
12
+ .clientExecute(async ({ input }) => {
13
+ return new Promise((resolve, reject) => {
14
+ const fileInput = document.createElement('input');
15
+ fileInput.type = 'file';
16
+ if (input.accept) fileInput.accept = input.accept;
17
+ if (input.multiple) fileInput.multiple = true;
18
+
19
+ fileInput.onchange = async (e) => {
20
+ const files = Array.from((e.target as HTMLInputElement).files || []);
21
+
22
+ if (input.maxSize) {
23
+ const oversized = files.filter(file => file.size > input.maxSize!);
24
+ if (oversized.length > 0) {
25
+ reject(new Error(`Files exceed max size: ${oversized.map(f => f.name).join(', ')}`));
26
+ return;
27
+ }
28
+ }
29
+
30
+ const fileData = await Promise.all(files.map(async file => ({
31
+ name: file.name,
32
+ size: file.size,
33
+ type: file.type,
34
+ lastModified: file.lastModified,
35
+ content: await file.text().catch(() => null)
36
+ })));
37
+
38
+ resolve({ files: fileData, count: fileData.length });
39
+ };
40
+
41
+ fileInput.click();
42
+ });
43
+ });
44
+
45
+ export const fileDownload = createAITool('file.download')
46
+ .describe('Download a file')
47
+ .tag('file', 'download', 'client')
48
+ .input(z.object({
49
+ content: z.string(),
50
+ filename: z.string(),
51
+ type: z.string().optional().default('text/plain')
52
+ }))
53
+ .clientExecute(async ({ input }) => {
54
+ const blob = new Blob([input.content], { type: input.type });
55
+ const url = URL.createObjectURL(blob);
56
+
57
+ const link = document.createElement('a');
58
+ link.href = url;
59
+ link.download = input.filename;
60
+ document.body.appendChild(link);
61
+ link.click();
62
+ document.body.removeChild(link);
63
+
64
+ URL.revokeObjectURL(url);
65
+
66
+ return { downloaded: true, filename: input.filename };
67
+ });
68
+
69
+ export const fileRead = createAITool('file.read')
70
+ .describe('Read file from input element')
71
+ .tag('file', 'read', 'client')
72
+ .input(z.object({
73
+ selector: z.string(),
74
+ readAs: z.enum(['text', 'dataURL', 'arrayBuffer']).optional().default('text')
75
+ }))
76
+ .clientExecute(async ({ input }) => {
77
+ const fileInput = document.querySelector(input.selector) as HTMLInputElement;
78
+ if (!fileInput) throw new Error(`Input not found: ${input.selector}`);
79
+ if (!fileInput.files || fileInput.files.length === 0) {
80
+ throw new Error('No files selected');
81
+ }
82
+
83
+ const file = fileInput.files[0];
84
+ const reader = new FileReader();
85
+
86
+ return new Promise((resolve, reject) => {
87
+ reader.onload = (e) => {
88
+ resolve({
89
+ name: file.name,
90
+ size: file.size,
91
+ type: file.type,
92
+ content: e.target?.result,
93
+ lastModified: file.lastModified
94
+ });
95
+ };
96
+
97
+ reader.onerror = () => reject(new Error('Failed to read file'));
98
+
99
+ switch (input.readAs) {
100
+ case 'dataURL':
101
+ reader.readAsDataURL(file);
102
+ break;
103
+ case 'arrayBuffer':
104
+ reader.readAsArrayBuffer(file);
105
+ break;
106
+ default:
107
+ reader.readAsText(file);
108
+ }
109
+ });
110
+ });
111
+
112
+ export const fileDragDrop = createAITool('file.dragDrop')
113
+ .describe('Setup drag and drop file handling')
114
+ .tag('file', 'upload', 'client')
115
+ .input(z.object({
116
+ selector: z.string(),
117
+ accept: z.string().optional()
118
+ }))
119
+ .clientExecute(async ({ input }) => {
120
+ const dropZone = document.querySelector(input.selector) as HTMLElement;
121
+ if (!dropZone) throw new Error(`Element not found: ${input.selector}`);
122
+
123
+ const dragId = Math.random().toString(36).substring(7);
124
+
125
+ return {
126
+ dragId,
127
+ onDrop: (callback: (files: File[]) => void) => {
128
+ const handleDragOver = (e: DragEvent) => {
129
+ e.preventDefault();
130
+ dropZone.classList.add('dragging');
131
+ };
132
+
133
+ const handleDragLeave = () => {
134
+ dropZone.classList.remove('dragging');
135
+ };
136
+
137
+ const handleDrop = (e: DragEvent) => {
138
+ e.preventDefault();
139
+ dropZone.classList.remove('dragging');
140
+
141
+ const files = Array.from(e.dataTransfer?.files || []);
142
+
143
+ if (input.accept) {
144
+ const acceptedTypes = input.accept.split(',').map(t => t.trim());
145
+ const validFiles = files.filter(file => {
146
+ return acceptedTypes.some(type => {
147
+ if (type.startsWith('.')) {
148
+ return file.name.endsWith(type);
149
+ }
150
+ return file.type.match(type.replace('*', '.*'));
151
+ });
152
+ });
153
+ callback(validFiles);
154
+ } else {
155
+ callback(files);
156
+ }
157
+ };
158
+
159
+ dropZone.addEventListener('dragover', handleDragOver);
160
+ dropZone.addEventListener('dragleave', handleDragLeave);
161
+ dropZone.addEventListener('drop', handleDrop);
162
+
163
+ return () => {
164
+ dropZone.removeEventListener('dragover', handleDragOver);
165
+ dropZone.removeEventListener('dragleave', handleDragLeave);
166
+ dropZone.removeEventListener('drop', handleDrop);
167
+ };
168
+ }
169
+ };
170
+ });
171
+
172
+ export const filePreview = createAITool('file.preview')
173
+ .describe('Preview file content')
174
+ .tag('file', 'preview', 'client')
175
+ .input(z.object({
176
+ file: z.any(),
177
+ targetSelector: z.string()
178
+ }))
179
+ .clientExecute(async ({ input }) => {
180
+ const target = document.querySelector(input.targetSelector) as HTMLElement;
181
+ if (!target) throw new Error(`Element not found: ${input.targetSelector}`);
182
+
183
+ const file = input.file;
184
+
185
+ if (file.type.startsWith('image/')) {
186
+ const img = document.createElement('img');
187
+ img.src = URL.createObjectURL(new Blob([file.content]));
188
+ img.style.maxWidth = '100%';
189
+ target.innerHTML = '';
190
+ target.appendChild(img);
191
+ } else if (file.type.startsWith('text/') || file.type === 'application/json') {
192
+ const pre = document.createElement('pre');
193
+ pre.textContent = file.content;
194
+ pre.style.cssText = 'white-space: pre-wrap; word-wrap: break-word;';
195
+ target.innerHTML = '';
196
+ target.appendChild(pre);
197
+ } else {
198
+ target.innerHTML = `<div>File: ${file.name} (${file.type})</div>`;
199
+ }
200
+
201
+ return { previewed: true, file: file.name };
202
+ });