agentic-astra-catalog 0.0.1
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 +154 -0
- package/app/api/db/objects/route.ts +44 -0
- package/app/api/tools/attributes/route.ts +27 -0
- package/app/api/tools/generate/route.ts +146 -0
- package/app/api/tools/route.ts +38 -0
- package/app/globals.css +4 -0
- package/app/layout.tsx +20 -0
- package/app/page.tsx +119 -0
- package/bin/agentic-astra-catalog.js +77 -0
- package/components/ThemeToggle.tsx +49 -0
- package/components/ToolEditor.tsx +1000 -0
- package/components/ToolList.tsx +75 -0
- package/lib/astraClient.ts +264 -0
- package/next.config.js +7 -0
- package/package.json +59 -0
- package/postcss.config.js +6 -0
- package/tailwind.config.js +12 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,1000 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from 'react';
|
|
4
|
+
import { Tool, ToolParameter } from '@/lib/astraClient';
|
|
5
|
+
|
|
6
|
+
interface ToolEditorProps {
|
|
7
|
+
tool: Tool | null;
|
|
8
|
+
onSave?: (savedTool: Tool) => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default function ToolEditor({ tool, onSave }: ToolEditorProps) {
|
|
12
|
+
const [formData, setFormData] = useState<Tool | null>(null);
|
|
13
|
+
const [saving, setSaving] = useState(false);
|
|
14
|
+
const [saveSuccess, setSaveSuccess] = useState(false);
|
|
15
|
+
const [dataType, setDataType] = useState<'collection' | 'table'>('collection');
|
|
16
|
+
const [availableAttributes, setAvailableAttributes] = useState<string[]>([]);
|
|
17
|
+
const [loadingAttributes, setLoadingAttributes] = useState(false);
|
|
18
|
+
const [isNewTool, setIsNewTool] = useState(false);
|
|
19
|
+
const [generating, setGenerating] = useState(false);
|
|
20
|
+
const [newToolData, setNewToolData] = useState({
|
|
21
|
+
dataType: 'collection' as 'collection' | 'table',
|
|
22
|
+
name: '',
|
|
23
|
+
dbName: '',
|
|
24
|
+
});
|
|
25
|
+
const [availableObjects, setAvailableObjects] = useState<string[]>([]);
|
|
26
|
+
const [loadingObjects, setLoadingObjects] = useState(false);
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (tool) {
|
|
30
|
+
// Check if this is a new tool (empty name and no _id)
|
|
31
|
+
const isEmptyTool = !tool.name && !tool._id && !tool.collection_name && !tool.table_name;
|
|
32
|
+
setIsNewTool(isEmptyTool);
|
|
33
|
+
|
|
34
|
+
if (isEmptyTool) {
|
|
35
|
+
// New tool - don't normalize yet
|
|
36
|
+
setFormData(tool);
|
|
37
|
+
setDataType('collection');
|
|
38
|
+
setAvailableAttributes([]);
|
|
39
|
+
// Initialize newToolData from tool if it has db_name
|
|
40
|
+
if (tool.db_name) {
|
|
41
|
+
setNewToolData({
|
|
42
|
+
dataType: 'collection',
|
|
43
|
+
name: '',
|
|
44
|
+
dbName: tool.db_name,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
// Load available objects
|
|
48
|
+
loadAvailableObjects('collection', tool.db_name);
|
|
49
|
+
} else {
|
|
50
|
+
// Existing tool - normalize parameters
|
|
51
|
+
const normalizedTool: Tool = {
|
|
52
|
+
...tool,
|
|
53
|
+
parameters: tool.parameters?.map((param): ToolParameter => {
|
|
54
|
+
// Determine paramMode based on existing fields
|
|
55
|
+
if (!param.paramMode) {
|
|
56
|
+
if (param.expr !== undefined && param.expr !== null && param.expr !== '') {
|
|
57
|
+
return { ...param, paramMode: 'expression' as const };
|
|
58
|
+
} else if (param.value !== undefined && param.value !== null && param.value !== '') {
|
|
59
|
+
return { ...param, paramMode: 'static' as const };
|
|
60
|
+
} else {
|
|
61
|
+
return { ...param, paramMode: 'tool_param' as const };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return param;
|
|
65
|
+
}) || []
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
setFormData(normalizedTool);
|
|
69
|
+
|
|
70
|
+
// Determine initial data type based on which field has a value
|
|
71
|
+
if (tool.collection_name) {
|
|
72
|
+
setDataType('collection');
|
|
73
|
+
} else if (tool.table_name) {
|
|
74
|
+
setDataType('table');
|
|
75
|
+
} else {
|
|
76
|
+
setDataType('collection'); // default
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Fetch attributes from sample documents
|
|
80
|
+
loadAttributes(tool);
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
setFormData(null);
|
|
84
|
+
setDataType('collection');
|
|
85
|
+
setAvailableAttributes([]);
|
|
86
|
+
setIsNewTool(false);
|
|
87
|
+
}
|
|
88
|
+
}, [tool]);
|
|
89
|
+
|
|
90
|
+
const loadAttributes = async (toolToLoad: Tool) => {
|
|
91
|
+
// Only fetch if we have collection_name or table_name and db_name
|
|
92
|
+
if (!toolToLoad.collection_name && !toolToLoad.table_name) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
setLoadingAttributes(true);
|
|
98
|
+
const response = await fetch('/api/tools/attributes', {
|
|
99
|
+
method: 'POST',
|
|
100
|
+
headers: {
|
|
101
|
+
'Content-Type': 'application/json',
|
|
102
|
+
},
|
|
103
|
+
body: JSON.stringify(toolToLoad),
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const data = await response.json();
|
|
107
|
+
|
|
108
|
+
if (response.ok && data.success) {
|
|
109
|
+
setAvailableAttributes(data.attributes || []);
|
|
110
|
+
}
|
|
111
|
+
} catch (error) {
|
|
112
|
+
console.error('Error loading attributes:', error);
|
|
113
|
+
// Don't show error to user, just log it
|
|
114
|
+
} finally {
|
|
115
|
+
setLoadingAttributes(false);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const loadAvailableObjects = async (dataType: 'collection' | 'table', dbName?: string) => {
|
|
120
|
+
try {
|
|
121
|
+
setLoadingObjects(true);
|
|
122
|
+
const type = dataType === 'collection' ? 'collections' : 'tables';
|
|
123
|
+
const url = `/api/db/objects?type=${type}${dbName ? `&dbName=${encodeURIComponent(dbName)}` : ''}`;
|
|
124
|
+
const response = await fetch(url);
|
|
125
|
+
const data = await response.json();
|
|
126
|
+
|
|
127
|
+
if (response.ok && data.success) {
|
|
128
|
+
setAvailableObjects(data.objects || []);
|
|
129
|
+
}
|
|
130
|
+
} catch (error) {
|
|
131
|
+
console.error('Error loading available objects:', error);
|
|
132
|
+
// Don't show error to user, just log it
|
|
133
|
+
} finally {
|
|
134
|
+
setLoadingObjects(false);
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const handleSave = async () => {
|
|
139
|
+
if (!formData) return;
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
setSaving(true);
|
|
143
|
+
const response = await fetch('/api/tools', {
|
|
144
|
+
method: 'POST',
|
|
145
|
+
headers: {
|
|
146
|
+
'Content-Type': 'application/json',
|
|
147
|
+
},
|
|
148
|
+
body: JSON.stringify(formData),
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const data = await response.json();
|
|
152
|
+
|
|
153
|
+
if (!response.ok || !data.success) {
|
|
154
|
+
throw new Error(data.error || 'Failed to save tool');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Update formData with the saved tool (in case _id was added)
|
|
158
|
+
const savedTool = { ...formData };
|
|
159
|
+
if (data.tool?._id) {
|
|
160
|
+
savedTool._id = data.tool._id;
|
|
161
|
+
}
|
|
162
|
+
setFormData(savedTool);
|
|
163
|
+
|
|
164
|
+
// Call the onSave callback to refresh the tool list
|
|
165
|
+
if (onSave) {
|
|
166
|
+
await onSave(savedTool);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Show success message
|
|
170
|
+
setSaveSuccess(true);
|
|
171
|
+
setTimeout(() => setSaveSuccess(false), 3000);
|
|
172
|
+
} catch (error) {
|
|
173
|
+
alert(`Error saving tool: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
174
|
+
} finally {
|
|
175
|
+
setSaving(false);
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const handleGenerateTool = async () => {
|
|
180
|
+
if (!newToolData.name || !newToolData.dataType) {
|
|
181
|
+
alert('Please provide data type and name');
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
setGenerating(true);
|
|
187
|
+
const response = await fetch('/api/tools/generate', {
|
|
188
|
+
method: 'POST',
|
|
189
|
+
headers: {
|
|
190
|
+
'Content-Type': 'application/json',
|
|
191
|
+
},
|
|
192
|
+
body: JSON.stringify({
|
|
193
|
+
dataType: newToolData.dataType,
|
|
194
|
+
name: newToolData.name,
|
|
195
|
+
dbName: newToolData.dbName,
|
|
196
|
+
}),
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const data = await response.json();
|
|
200
|
+
|
|
201
|
+
if (!response.ok || !data.success) {
|
|
202
|
+
throw new Error(data.error || 'Failed to generate tool');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Set the generated tool as formData
|
|
206
|
+
const generatedTool = data.tool;
|
|
207
|
+
setFormData(generatedTool);
|
|
208
|
+
setIsNewTool(false);
|
|
209
|
+
|
|
210
|
+
// Update dataType and load attributes
|
|
211
|
+
setDataType(newToolData.dataType);
|
|
212
|
+
await loadAttributes(generatedTool);
|
|
213
|
+
} catch (error) {
|
|
214
|
+
alert(`Error generating tool: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
215
|
+
} finally {
|
|
216
|
+
setGenerating(false);
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
if (!formData) {
|
|
221
|
+
return (
|
|
222
|
+
<div className="flex-1 flex items-center justify-center bg-white dark:bg-gray-800">
|
|
223
|
+
<div className="text-center text-gray-500 dark:text-gray-400">
|
|
224
|
+
<p className="text-lg">Select a tool from the list to view and edit</p>
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Show new tool creation form
|
|
231
|
+
if (isNewTool) {
|
|
232
|
+
return (
|
|
233
|
+
<div className="flex-1 overflow-y-auto bg-white dark:bg-gray-800">
|
|
234
|
+
<div className="max-w-2xl mx-auto p-6">
|
|
235
|
+
<h1 className="text-2xl font-bold text-gray-800 dark:text-gray-200 mb-6">Create New Tool</h1>
|
|
236
|
+
|
|
237
|
+
<div className="bg-gray-50 dark:bg-gray-900 rounded-lg p-6 space-y-4">
|
|
238
|
+
<div>
|
|
239
|
+
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
240
|
+
Data Type *
|
|
241
|
+
</label>
|
|
242
|
+
<select
|
|
243
|
+
value={newToolData.dataType}
|
|
244
|
+
onChange={(e) => {
|
|
245
|
+
const newDataType = e.target.value as 'collection' | 'table';
|
|
246
|
+
setNewToolData({ ...newToolData, dataType: newDataType, name: '' });
|
|
247
|
+
loadAvailableObjects(newDataType, newToolData.dbName);
|
|
248
|
+
}}
|
|
249
|
+
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200"
|
|
250
|
+
>
|
|
251
|
+
<option value="collection">Collection</option>
|
|
252
|
+
<option value="table">Table</option>
|
|
253
|
+
</select>
|
|
254
|
+
</div>
|
|
255
|
+
|
|
256
|
+
<div>
|
|
257
|
+
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
258
|
+
{newToolData.dataType === 'collection' ? 'Collection Name' : 'Table Name'} *
|
|
259
|
+
</label>
|
|
260
|
+
{availableObjects.length > 0 ? (
|
|
261
|
+
<select
|
|
262
|
+
value={newToolData.name}
|
|
263
|
+
onChange={(e) => setNewToolData({ ...newToolData, name: e.target.value })}
|
|
264
|
+
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200"
|
|
265
|
+
>
|
|
266
|
+
<option value="">-- Select {newToolData.dataType} --</option>
|
|
267
|
+
{availableObjects.map((obj) => (
|
|
268
|
+
<option key={obj} value={obj}>
|
|
269
|
+
{obj}
|
|
270
|
+
</option>
|
|
271
|
+
))}
|
|
272
|
+
</select>
|
|
273
|
+
) : (
|
|
274
|
+
<input
|
|
275
|
+
type="text"
|
|
276
|
+
value={newToolData.name}
|
|
277
|
+
onChange={(e) => setNewToolData({ ...newToolData, name: e.target.value })}
|
|
278
|
+
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200"
|
|
279
|
+
placeholder={loadingObjects ? `Loading ${newToolData.dataType}s...` : `Enter ${newToolData.dataType} name`}
|
|
280
|
+
/>
|
|
281
|
+
)}
|
|
282
|
+
</div>
|
|
283
|
+
|
|
284
|
+
<div>
|
|
285
|
+
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
286
|
+
Database Name
|
|
287
|
+
</label>
|
|
288
|
+
<input
|
|
289
|
+
type="text"
|
|
290
|
+
value={newToolData.dbName}
|
|
291
|
+
onChange={(e) => {
|
|
292
|
+
const newDbName = e.target.value;
|
|
293
|
+
setNewToolData({ ...newToolData, dbName: newDbName, name: '' });
|
|
294
|
+
if (newDbName) {
|
|
295
|
+
loadAvailableObjects(newToolData.dataType, newDbName);
|
|
296
|
+
} else {
|
|
297
|
+
loadAvailableObjects(newToolData.dataType);
|
|
298
|
+
}
|
|
299
|
+
}}
|
|
300
|
+
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200"
|
|
301
|
+
placeholder="Leave empty to use default"
|
|
302
|
+
/>
|
|
303
|
+
</div>
|
|
304
|
+
|
|
305
|
+
<button
|
|
306
|
+
onClick={handleGenerateTool}
|
|
307
|
+
disabled={generating || !newToolData.name}
|
|
308
|
+
className="w-full px-4 py-3 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed font-medium"
|
|
309
|
+
>
|
|
310
|
+
{generating ? (
|
|
311
|
+
<span className="flex items-center justify-center">
|
|
312
|
+
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
313
|
+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
|
314
|
+
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
315
|
+
</svg>
|
|
316
|
+
Generating tool with AI...
|
|
317
|
+
</span>
|
|
318
|
+
) : (
|
|
319
|
+
'Generate Tool with AI'
|
|
320
|
+
)}
|
|
321
|
+
</button>
|
|
322
|
+
</div>
|
|
323
|
+
</div>
|
|
324
|
+
</div>
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const updateField = (field: keyof Tool, value: any) => {
|
|
329
|
+
const updated = { ...formData, [field]: value };
|
|
330
|
+
setFormData(updated);
|
|
331
|
+
|
|
332
|
+
// If collection_name, table_name, or db_name changed, reload attributes
|
|
333
|
+
if ((field === 'collection_name' || field === 'table_name' || field === 'db_name') && updated) {
|
|
334
|
+
if (updated.collection_name || updated.table_name) {
|
|
335
|
+
loadAttributes(updated);
|
|
336
|
+
} else {
|
|
337
|
+
setAvailableAttributes([]);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
const updateParameter = (index: number, field: keyof ToolParameter, value: any) => {
|
|
343
|
+
if (!formData.parameters) return;
|
|
344
|
+
const newParameters = [...formData.parameters];
|
|
345
|
+
newParameters[index] = { ...newParameters[index], [field]: value };
|
|
346
|
+
setFormData({ ...formData, parameters: newParameters });
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
const addParameter = () => {
|
|
350
|
+
const newParam: ToolParameter = {
|
|
351
|
+
param: '',
|
|
352
|
+
description: '',
|
|
353
|
+
type: 'string',
|
|
354
|
+
required: false,
|
|
355
|
+
paramMode: 'tool_param',
|
|
356
|
+
};
|
|
357
|
+
setFormData({
|
|
358
|
+
...formData,
|
|
359
|
+
parameters: [...(formData.parameters || []), newParam],
|
|
360
|
+
});
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
const removeParameter = (index: number) => {
|
|
364
|
+
if (!formData.parameters) return;
|
|
365
|
+
const newParameters = formData.parameters.filter((_, i) => i !== index);
|
|
366
|
+
setFormData({ ...formData, parameters: newParameters });
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
const updateTags = (tagsString: string) => {
|
|
370
|
+
const tags = tagsString.split(',').map(t => t.trim()).filter(t => t);
|
|
371
|
+
updateField('tags', tags);
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
const addProjectionField = () => {
|
|
375
|
+
const currentProjection = formData.projection || {};
|
|
376
|
+
const newField = `field_${Object.keys(currentProjection).length + 1}`;
|
|
377
|
+
setFormData({
|
|
378
|
+
...formData,
|
|
379
|
+
projection: { ...currentProjection, [newField]: 1 }
|
|
380
|
+
});
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
const removeProjectionField = (fieldName: string) => {
|
|
384
|
+
if (!formData.projection) return;
|
|
385
|
+
const newProjection = { ...formData.projection };
|
|
386
|
+
delete newProjection[fieldName];
|
|
387
|
+
setFormData({ ...formData, projection: Object.keys(newProjection).length > 0 ? newProjection : undefined });
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
const updateProjectionField = (oldFieldName: string, newFieldName: string, value: number | string) => {
|
|
391
|
+
if (!formData.projection) return;
|
|
392
|
+
const newProjection: Record<string, number | string> = { ...formData.projection };
|
|
393
|
+
|
|
394
|
+
// If field name changed, remove old and add new
|
|
395
|
+
if (oldFieldName !== newFieldName) {
|
|
396
|
+
delete newProjection[oldFieldName];
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
newProjection[newFieldName] = value;
|
|
400
|
+
setFormData({ ...formData, projection: newProjection });
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
return (
|
|
404
|
+
<div className="flex-1 overflow-y-auto bg-white dark:bg-gray-800">
|
|
405
|
+
<div className="max-w-4xl mx-auto p-6">
|
|
406
|
+
<div className="flex justify-between items-center mb-6">
|
|
407
|
+
<h1 className="text-2xl font-bold text-gray-800 dark:text-gray-200">Edit Tool</h1>
|
|
408
|
+
<div className="flex items-center gap-3">
|
|
409
|
+
{saveSuccess && (
|
|
410
|
+
<span className="text-sm text-green-600 dark:text-green-400 font-medium">
|
|
411
|
+
✓ Saved successfully
|
|
412
|
+
</span>
|
|
413
|
+
)}
|
|
414
|
+
<button
|
|
415
|
+
onClick={handleSave}
|
|
416
|
+
disabled={saving}
|
|
417
|
+
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
418
|
+
>
|
|
419
|
+
{saving ? 'Saving...' : 'Save Tool'}
|
|
420
|
+
</button>
|
|
421
|
+
</div>
|
|
422
|
+
</div>
|
|
423
|
+
|
|
424
|
+
<div className="space-y-6">
|
|
425
|
+
{/* Basic Information */}
|
|
426
|
+
<div className="bg-gray-50 dark:bg-gray-900 rounded-lg p-4">
|
|
427
|
+
<h2 className="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-4">Basic Information</h2>
|
|
428
|
+
<div className="space-y-4">
|
|
429
|
+
<div className="flex items-center space-x-3 p-3 bg-white dark:bg-gray-800 rounded-lg border border-gray-300 dark:border-gray-600">
|
|
430
|
+
<label className="flex items-center space-x-2 cursor-pointer">
|
|
431
|
+
<input
|
|
432
|
+
type="checkbox"
|
|
433
|
+
checked={formData.enabled !== false}
|
|
434
|
+
onChange={(e) => updateField('enabled', e.target.checked)}
|
|
435
|
+
className="w-5 h-5 rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500"
|
|
436
|
+
/>
|
|
437
|
+
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
438
|
+
Tool Enabled
|
|
439
|
+
</span>
|
|
440
|
+
</label>
|
|
441
|
+
{formData.enabled === false && (
|
|
442
|
+
<span className="text-xs px-2 py-1 rounded bg-red-100 dark:bg-red-900 text-red-700 dark:text-red-300">
|
|
443
|
+
Disabled
|
|
444
|
+
</span>
|
|
445
|
+
)}
|
|
446
|
+
</div>
|
|
447
|
+
|
|
448
|
+
<div>
|
|
449
|
+
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
450
|
+
Name *
|
|
451
|
+
</label>
|
|
452
|
+
<input
|
|
453
|
+
type="text"
|
|
454
|
+
value={formData.name || ''}
|
|
455
|
+
onChange={(e) => updateField('name', e.target.value)}
|
|
456
|
+
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200"
|
|
457
|
+
/>
|
|
458
|
+
</div>
|
|
459
|
+
|
|
460
|
+
<div>
|
|
461
|
+
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
462
|
+
Description
|
|
463
|
+
</label>
|
|
464
|
+
<textarea
|
|
465
|
+
value={formData.description || ''}
|
|
466
|
+
onChange={(e) => updateField('description', e.target.value)}
|
|
467
|
+
rows={3}
|
|
468
|
+
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200"
|
|
469
|
+
/>
|
|
470
|
+
</div>
|
|
471
|
+
|
|
472
|
+
<div>
|
|
473
|
+
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
474
|
+
Tags (comma-separated)
|
|
475
|
+
</label>
|
|
476
|
+
<input
|
|
477
|
+
type="text"
|
|
478
|
+
value={formData.tags?.join(', ') || ''}
|
|
479
|
+
onChange={(e) => updateTags(e.target.value)}
|
|
480
|
+
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200"
|
|
481
|
+
placeholder="products, ecommerce, clothing"
|
|
482
|
+
/>
|
|
483
|
+
</div>
|
|
484
|
+
|
|
485
|
+
<div>
|
|
486
|
+
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
487
|
+
Type
|
|
488
|
+
</label>
|
|
489
|
+
<input
|
|
490
|
+
type="text"
|
|
491
|
+
value={formData.type || ''}
|
|
492
|
+
onChange={(e) => updateField('type', e.target.value)}
|
|
493
|
+
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200"
|
|
494
|
+
/>
|
|
495
|
+
</div>
|
|
496
|
+
</div>
|
|
497
|
+
</div>
|
|
498
|
+
|
|
499
|
+
{/* Method & Collection */}
|
|
500
|
+
<div className="bg-gray-50 dark:bg-gray-900 rounded-lg p-4">
|
|
501
|
+
<h2 className="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-4">Database Configuration</h2>
|
|
502
|
+
<div className="space-y-4">
|
|
503
|
+
<div>
|
|
504
|
+
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
505
|
+
DB Name
|
|
506
|
+
</label>
|
|
507
|
+
<input
|
|
508
|
+
type="text"
|
|
509
|
+
value={formData.db_name || ''}
|
|
510
|
+
onChange={(e) => updateField('db_name', e.target.value)}
|
|
511
|
+
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200"
|
|
512
|
+
/>
|
|
513
|
+
</div>
|
|
514
|
+
|
|
515
|
+
<div className="grid grid-cols-2 gap-4">
|
|
516
|
+
<div>
|
|
517
|
+
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
518
|
+
Data Type
|
|
519
|
+
</label>
|
|
520
|
+
<select
|
|
521
|
+
value={dataType}
|
|
522
|
+
onChange={(e) => {
|
|
523
|
+
const newType = e.target.value as 'collection' | 'table';
|
|
524
|
+
setDataType(newType);
|
|
525
|
+
// Clear the other field when switching
|
|
526
|
+
if (newType === 'collection') {
|
|
527
|
+
updateField('table_name', undefined);
|
|
528
|
+
} else {
|
|
529
|
+
updateField('collection_name', undefined);
|
|
530
|
+
}
|
|
531
|
+
}}
|
|
532
|
+
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200"
|
|
533
|
+
>
|
|
534
|
+
<option value="collection">Collection</option>
|
|
535
|
+
<option value="table">Table</option>
|
|
536
|
+
</select>
|
|
537
|
+
</div>
|
|
538
|
+
|
|
539
|
+
{dataType === 'collection' ? (
|
|
540
|
+
<div>
|
|
541
|
+
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
542
|
+
Collection Name
|
|
543
|
+
</label>
|
|
544
|
+
<input
|
|
545
|
+
type="text"
|
|
546
|
+
value={formData.collection_name || ''}
|
|
547
|
+
onChange={(e) => updateField('collection_name', e.target.value)}
|
|
548
|
+
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200"
|
|
549
|
+
/>
|
|
550
|
+
</div>
|
|
551
|
+
) : (
|
|
552
|
+
<div>
|
|
553
|
+
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
554
|
+
Table Name
|
|
555
|
+
</label>
|
|
556
|
+
<input
|
|
557
|
+
type="text"
|
|
558
|
+
value={formData.table_name || ''}
|
|
559
|
+
onChange={(e) => updateField('table_name', e.target.value)}
|
|
560
|
+
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200"
|
|
561
|
+
/>
|
|
562
|
+
</div>
|
|
563
|
+
)}
|
|
564
|
+
</div>
|
|
565
|
+
|
|
566
|
+
<div>
|
|
567
|
+
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
568
|
+
Method
|
|
569
|
+
</label>
|
|
570
|
+
<input
|
|
571
|
+
type="text"
|
|
572
|
+
value={formData.method || ''}
|
|
573
|
+
onChange={(e) => updateField('method', e.target.value)}
|
|
574
|
+
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200"
|
|
575
|
+
placeholder="find, find_documents"
|
|
576
|
+
/>
|
|
577
|
+
</div>
|
|
578
|
+
</div>
|
|
579
|
+
</div>
|
|
580
|
+
|
|
581
|
+
{/* Projection */}
|
|
582
|
+
<div className="bg-gray-50 dark:bg-gray-900 rounded-lg p-4">
|
|
583
|
+
<div className="flex justify-between items-center mb-4">
|
|
584
|
+
<h2 className="text-lg font-semibold text-gray-800 dark:text-gray-200">Projection</h2>
|
|
585
|
+
<div className="flex gap-2">
|
|
586
|
+
{formData && (formData.collection_name || formData.table_name) && (
|
|
587
|
+
<button
|
|
588
|
+
onClick={() => formData && loadAttributes(formData)}
|
|
589
|
+
disabled={loadingAttributes}
|
|
590
|
+
className="px-3 py-1 bg-gray-600 text-white rounded-md hover:bg-gray-700 transition-colors text-sm disabled:opacity-50 disabled:cursor-not-allowed"
|
|
591
|
+
title="Refresh attributes from collection/table"
|
|
592
|
+
>
|
|
593
|
+
{loadingAttributes ? 'Loading...' : 'Refresh Attributes'}
|
|
594
|
+
</button>
|
|
595
|
+
)}
|
|
596
|
+
<button
|
|
597
|
+
onClick={addProjectionField}
|
|
598
|
+
className="px-3 py-1 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors text-sm"
|
|
599
|
+
>
|
|
600
|
+
Add Field
|
|
601
|
+
</button>
|
|
602
|
+
</div>
|
|
603
|
+
</div>
|
|
604
|
+
{availableAttributes.length > 0 && (
|
|
605
|
+
<div className="mb-3 text-xs text-gray-600 dark:text-gray-400">
|
|
606
|
+
Found {availableAttributes.length} attribute{availableAttributes.length !== 1 ? 's' : ''} from sample documents
|
|
607
|
+
</div>
|
|
608
|
+
)}
|
|
609
|
+
<div className="space-y-4">
|
|
610
|
+
{formData.projection && Object.keys(formData.projection).length > 0 ? (
|
|
611
|
+
<div className="overflow-x-auto">
|
|
612
|
+
<table className="w-full border-collapse border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800">
|
|
613
|
+
<thead>
|
|
614
|
+
<tr className="bg-gray-100 dark:bg-gray-700">
|
|
615
|
+
<th className="border border-gray-300 dark:border-gray-600 px-3 py-2 text-left text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
616
|
+
Attribute
|
|
617
|
+
</th>
|
|
618
|
+
<th className="border border-gray-300 dark:border-gray-600 px-3 py-2 text-left text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
619
|
+
Value
|
|
620
|
+
</th>
|
|
621
|
+
<th className="border border-gray-300 dark:border-gray-600 px-3 py-2 text-center text-sm font-medium text-gray-700 dark:text-gray-300 w-24">
|
|
622
|
+
Actions
|
|
623
|
+
</th>
|
|
624
|
+
</tr>
|
|
625
|
+
</thead>
|
|
626
|
+
<tbody>
|
|
627
|
+
{Object.entries(formData.projection).map(([fieldName, value]) => (
|
|
628
|
+
<tr key={fieldName} className="hover:bg-gray-50 dark:hover:bg-gray-700">
|
|
629
|
+
<td className="border border-gray-300 dark:border-gray-600 px-3 py-2">
|
|
630
|
+
{availableAttributes.length > 0 ? (
|
|
631
|
+
<select
|
|
632
|
+
value={fieldName}
|
|
633
|
+
onChange={(e) => {
|
|
634
|
+
const newName = e.target.value;
|
|
635
|
+
updateProjectionField(fieldName, newName, value);
|
|
636
|
+
}}
|
|
637
|
+
className="w-full px-2 py-1 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200 text-sm"
|
|
638
|
+
>
|
|
639
|
+
<option value="">-- Select attribute --</option>
|
|
640
|
+
{availableAttributes.map((attr) => (
|
|
641
|
+
<option key={attr} value={attr}>
|
|
642
|
+
{attr}
|
|
643
|
+
</option>
|
|
644
|
+
))}
|
|
645
|
+
{!availableAttributes.includes(fieldName) && fieldName && (
|
|
646
|
+
<option value={fieldName}>{fieldName} (custom)</option>
|
|
647
|
+
)}
|
|
648
|
+
</select>
|
|
649
|
+
) : (
|
|
650
|
+
<input
|
|
651
|
+
type="text"
|
|
652
|
+
value={fieldName}
|
|
653
|
+
onChange={(e) => {
|
|
654
|
+
const newName = e.target.value;
|
|
655
|
+
updateProjectionField(fieldName, newName, value);
|
|
656
|
+
}}
|
|
657
|
+
className="w-full px-2 py-1 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200 text-sm"
|
|
658
|
+
placeholder={loadingAttributes ? 'Loading attributes...' : 'Enter attribute name'}
|
|
659
|
+
/>
|
|
660
|
+
)}
|
|
661
|
+
</td>
|
|
662
|
+
<td className="border border-gray-300 dark:border-gray-600 px-3 py-2">
|
|
663
|
+
{typeof value === 'number' || (typeof value === 'string' && (value === '1' || value === '0')) ? (
|
|
664
|
+
<select
|
|
665
|
+
value={typeof value === 'number' ? value.toString() : value}
|
|
666
|
+
onChange={(e) => {
|
|
667
|
+
const val = e.target.value;
|
|
668
|
+
let newValue: number | string;
|
|
669
|
+
if (val === '1') {
|
|
670
|
+
newValue = 1;
|
|
671
|
+
} else if (val === '0') {
|
|
672
|
+
newValue = 0;
|
|
673
|
+
} else {
|
|
674
|
+
// Custom value - keep current value or set empty string
|
|
675
|
+
newValue = typeof value === 'string' && value !== '1' && value !== '0' ? value : '';
|
|
676
|
+
}
|
|
677
|
+
updateProjectionField(fieldName, fieldName, newValue);
|
|
678
|
+
}}
|
|
679
|
+
className="w-full px-2 py-1 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200 text-sm"
|
|
680
|
+
>
|
|
681
|
+
<option value="1">1 (Enable)</option>
|
|
682
|
+
<option value="0">0 (Disable)</option>
|
|
683
|
+
<option value="custom">Custom Name/Value</option>
|
|
684
|
+
</select>
|
|
685
|
+
) : (
|
|
686
|
+
<div className="space-y-2">
|
|
687
|
+
<select
|
|
688
|
+
value="custom"
|
|
689
|
+
onChange={(e) => {
|
|
690
|
+
const val = e.target.value;
|
|
691
|
+
if (val === '1') {
|
|
692
|
+
updateProjectionField(fieldName, fieldName, 1);
|
|
693
|
+
} else if (val === '0') {
|
|
694
|
+
updateProjectionField(fieldName, fieldName, 0);
|
|
695
|
+
}
|
|
696
|
+
}}
|
|
697
|
+
className="w-full px-2 py-1 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200 text-sm"
|
|
698
|
+
>
|
|
699
|
+
<option value="custom">Custom Name/Value</option>
|
|
700
|
+
<option value="1">1 (Enable)</option>
|
|
701
|
+
<option value="0">0 (Disable)</option>
|
|
702
|
+
</select>
|
|
703
|
+
<input
|
|
704
|
+
type="text"
|
|
705
|
+
value={value}
|
|
706
|
+
onChange={(e) => updateProjectionField(fieldName, fieldName, e.target.value)}
|
|
707
|
+
className="w-full px-2 py-1 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200 text-sm"
|
|
708
|
+
placeholder="Enter custom value"
|
|
709
|
+
/>
|
|
710
|
+
</div>
|
|
711
|
+
)}
|
|
712
|
+
</td>
|
|
713
|
+
<td className="border border-gray-300 dark:border-gray-600 px-3 py-2 text-center">
|
|
714
|
+
<button
|
|
715
|
+
onClick={() => removeProjectionField(fieldName)}
|
|
716
|
+
className="text-red-600 dark:text-red-400 hover:text-red-700 dark:hover:text-red-300 text-sm"
|
|
717
|
+
>
|
|
718
|
+
Remove
|
|
719
|
+
</button>
|
|
720
|
+
</td>
|
|
721
|
+
</tr>
|
|
722
|
+
))}
|
|
723
|
+
</tbody>
|
|
724
|
+
</table>
|
|
725
|
+
</div>
|
|
726
|
+
) : (
|
|
727
|
+
<div className="text-center py-8 text-gray-500 dark:text-gray-400 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800">
|
|
728
|
+
<p>No projection fields defined. Click "Add Field" to add one.</p>
|
|
729
|
+
</div>
|
|
730
|
+
)}
|
|
731
|
+
<div>
|
|
732
|
+
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
733
|
+
Limit
|
|
734
|
+
</label>
|
|
735
|
+
<input
|
|
736
|
+
type="number"
|
|
737
|
+
value={formData.limit || ''}
|
|
738
|
+
onChange={(e) => updateField('limit', e.target.value ? parseInt(e.target.value) : undefined)}
|
|
739
|
+
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200"
|
|
740
|
+
/>
|
|
741
|
+
</div>
|
|
742
|
+
</div>
|
|
743
|
+
</div>
|
|
744
|
+
|
|
745
|
+
{/* Parameters */}
|
|
746
|
+
<div className="bg-gray-50 dark:bg-gray-900 rounded-lg p-4">
|
|
747
|
+
<div className="flex justify-between items-center mb-4">
|
|
748
|
+
<h2 className="text-lg font-semibold text-gray-800 dark:text-gray-200">Parameters</h2>
|
|
749
|
+
<button
|
|
750
|
+
onClick={addParameter}
|
|
751
|
+
className="px-3 py-1 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors text-sm"
|
|
752
|
+
>
|
|
753
|
+
Add Parameter
|
|
754
|
+
</button>
|
|
755
|
+
</div>
|
|
756
|
+
|
|
757
|
+
{formData.parameters && formData.parameters.length > 0 ? (
|
|
758
|
+
<div className="space-y-4">
|
|
759
|
+
{formData.parameters.map((param, index) => (
|
|
760
|
+
<div key={index} className="border border-gray-300 dark:border-gray-600 rounded-lg p-4 bg-white dark:bg-gray-800">
|
|
761
|
+
<div className="flex justify-between items-start mb-3">
|
|
762
|
+
<h3 className="font-medium text-gray-800 dark:text-gray-200">Parameter {index + 1}</h3>
|
|
763
|
+
<button
|
|
764
|
+
onClick={() => removeParameter(index)}
|
|
765
|
+
className="text-red-600 dark:text-red-400 hover:text-red-700 dark:hover:text-red-300 text-sm"
|
|
766
|
+
>
|
|
767
|
+
Remove
|
|
768
|
+
</button>
|
|
769
|
+
</div>
|
|
770
|
+
|
|
771
|
+
{/* 1. Parameter Type and 6. Required on same row */}
|
|
772
|
+
<div className="grid grid-cols-2 gap-4 mb-3">
|
|
773
|
+
<div>
|
|
774
|
+
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
775
|
+
Parameter Type *
|
|
776
|
+
</label>
|
|
777
|
+
<select
|
|
778
|
+
value={param.paramMode || 'tool_param'}
|
|
779
|
+
onChange={(e) => updateParameter(index, 'paramMode', e.target.value)}
|
|
780
|
+
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200"
|
|
781
|
+
>
|
|
782
|
+
<option value="tool_param">Tool Param</option>
|
|
783
|
+
<option value="static">Static Value</option>
|
|
784
|
+
<option value="expression">Expression</option>
|
|
785
|
+
</select>
|
|
786
|
+
</div>
|
|
787
|
+
|
|
788
|
+
{param.paramMode === 'tool_param' && (
|
|
789
|
+
<div>
|
|
790
|
+
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
791
|
+
Required
|
|
792
|
+
</label>
|
|
793
|
+
<div className="flex items-center h-10">
|
|
794
|
+
<label className="flex items-center space-x-2 cursor-pointer">
|
|
795
|
+
<input
|
|
796
|
+
type="checkbox"
|
|
797
|
+
checked={!!param.required}
|
|
798
|
+
onChange={(e) => updateParameter(index, 'required', e.target.checked)}
|
|
799
|
+
className="rounded border-gray-300 dark:border-gray-600"
|
|
800
|
+
/>
|
|
801
|
+
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">Required</span>
|
|
802
|
+
</label>
|
|
803
|
+
</div>
|
|
804
|
+
</div>
|
|
805
|
+
)}
|
|
806
|
+
</div>
|
|
807
|
+
|
|
808
|
+
{/* 4. Attribute and 2. Param Name on same row */}
|
|
809
|
+
<div className="grid grid-cols-2 gap-4 mb-3">
|
|
810
|
+
<div>
|
|
811
|
+
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
812
|
+
Attribute *
|
|
813
|
+
</label>
|
|
814
|
+
{availableAttributes.length > 0 ? (
|
|
815
|
+
<select
|
|
816
|
+
value={param.attribute || ''}
|
|
817
|
+
onChange={(e) => updateParameter(index, 'attribute', e.target.value)}
|
|
818
|
+
required
|
|
819
|
+
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200"
|
|
820
|
+
>
|
|
821
|
+
<option value="">-- Select attribute --</option>
|
|
822
|
+
{availableAttributes.map((attr) => (
|
|
823
|
+
<option key={attr} value={attr}>
|
|
824
|
+
{attr}
|
|
825
|
+
</option>
|
|
826
|
+
))}
|
|
827
|
+
{param.attribute && !availableAttributes.includes(param.attribute) && (
|
|
828
|
+
<option value={param.attribute}>{param.attribute} (custom)</option>
|
|
829
|
+
)}
|
|
830
|
+
</select>
|
|
831
|
+
) : (
|
|
832
|
+
<input
|
|
833
|
+
type="text"
|
|
834
|
+
value={param.attribute || ''}
|
|
835
|
+
onChange={(e) => updateParameter(index, 'attribute', e.target.value)}
|
|
836
|
+
required
|
|
837
|
+
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200"
|
|
838
|
+
placeholder={loadingAttributes ? 'Loading attributes...' : '$vectorize, $vector, or column name'}
|
|
839
|
+
/>
|
|
840
|
+
)}
|
|
841
|
+
</div>
|
|
842
|
+
|
|
843
|
+
<div>
|
|
844
|
+
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
845
|
+
Param Name
|
|
846
|
+
</label>
|
|
847
|
+
<input
|
|
848
|
+
type="text"
|
|
849
|
+
value={param.param || ''}
|
|
850
|
+
onChange={(e) => updateParameter(index, 'param', e.target.value)}
|
|
851
|
+
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200"
|
|
852
|
+
/>
|
|
853
|
+
</div>
|
|
854
|
+
</div>
|
|
855
|
+
|
|
856
|
+
{/* 3. Type (data type) and 5. Operator on same row */}
|
|
857
|
+
<div className="grid grid-cols-2 gap-4 mb-3">
|
|
858
|
+
{param.paramMode === 'tool_param' && (
|
|
859
|
+
<div>
|
|
860
|
+
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
861
|
+
Type
|
|
862
|
+
</label>
|
|
863
|
+
<select
|
|
864
|
+
value={param.type || 'string'}
|
|
865
|
+
onChange={(e) => updateParameter(index, 'type', e.target.value)}
|
|
866
|
+
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200"
|
|
867
|
+
>
|
|
868
|
+
<option value="string">string</option>
|
|
869
|
+
<option value="number">number</option>
|
|
870
|
+
<option value="boolean">boolean</option>
|
|
871
|
+
<option value="text">text</option>
|
|
872
|
+
<option value="timestamp">timestamp</option>
|
|
873
|
+
<option value="float">float</option>
|
|
874
|
+
<option value="vector">vector</option>
|
|
875
|
+
</select>
|
|
876
|
+
</div>
|
|
877
|
+
)}
|
|
878
|
+
|
|
879
|
+
<div>
|
|
880
|
+
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
881
|
+
Operator
|
|
882
|
+
</label>
|
|
883
|
+
<select
|
|
884
|
+
value={param.operator || '$eq'}
|
|
885
|
+
onChange={(e) => updateParameter(index, 'operator', e.target.value)}
|
|
886
|
+
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200"
|
|
887
|
+
>
|
|
888
|
+
<option value="$eq">$eq</option>
|
|
889
|
+
<option value="$gt">$gt</option>
|
|
890
|
+
<option value="$gte">$gte</option>
|
|
891
|
+
<option value="$lt">$lt</option>
|
|
892
|
+
<option value="$lte">$lte</option>
|
|
893
|
+
<option value="$in">$in</option>
|
|
894
|
+
<option value="$ne">$ne</option>
|
|
895
|
+
</select>
|
|
896
|
+
</div>
|
|
897
|
+
</div>
|
|
898
|
+
|
|
899
|
+
{/* Description (only for tool_param) */}
|
|
900
|
+
{param.paramMode === 'tool_param' && (
|
|
901
|
+
<div className="mb-3">
|
|
902
|
+
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
903
|
+
Description
|
|
904
|
+
</label>
|
|
905
|
+
<textarea
|
|
906
|
+
value={param.description || ''}
|
|
907
|
+
onChange={(e) => updateParameter(index, 'description', e.target.value)}
|
|
908
|
+
rows={2}
|
|
909
|
+
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200"
|
|
910
|
+
/>
|
|
911
|
+
</div>
|
|
912
|
+
)}
|
|
913
|
+
|
|
914
|
+
{/* 7. Static Value (only for static mode) */}
|
|
915
|
+
{param.paramMode === 'static' && (
|
|
916
|
+
<div className="mb-3">
|
|
917
|
+
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
918
|
+
Static Value
|
|
919
|
+
</label>
|
|
920
|
+
<textarea
|
|
921
|
+
value={param.value !== undefined && param.value !== null ? String(param.value) : ''}
|
|
922
|
+
onChange={(e) => {
|
|
923
|
+
// Try to parse as JSON, otherwise store as string
|
|
924
|
+
let parsedValue: any = e.target.value;
|
|
925
|
+
try {
|
|
926
|
+
parsedValue = JSON.parse(e.target.value);
|
|
927
|
+
} catch {
|
|
928
|
+
// Keep as string if not valid JSON
|
|
929
|
+
}
|
|
930
|
+
updateParameter(index, 'value', parsedValue);
|
|
931
|
+
}}
|
|
932
|
+
rows={3}
|
|
933
|
+
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200 font-mono text-sm"
|
|
934
|
+
placeholder="Enter any value (JSON will be parsed if valid)"
|
|
935
|
+
/>
|
|
936
|
+
</div>
|
|
937
|
+
)}
|
|
938
|
+
|
|
939
|
+
{/* 8. Expression (only for expression mode) */}
|
|
940
|
+
{param.paramMode === 'expression' && (
|
|
941
|
+
<div className="mb-3">
|
|
942
|
+
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
943
|
+
Python Expression
|
|
944
|
+
</label>
|
|
945
|
+
<textarea
|
|
946
|
+
value={param.expr || ''}
|
|
947
|
+
onChange={(e) => updateParameter(index, 'expr', e.target.value)}
|
|
948
|
+
rows={3}
|
|
949
|
+
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200 font-mono text-sm"
|
|
950
|
+
placeholder="e.g., len(context.get('query', ''))"
|
|
951
|
+
/>
|
|
952
|
+
</div>
|
|
953
|
+
)}
|
|
954
|
+
|
|
955
|
+
|
|
956
|
+
{/* 9. Info */}
|
|
957
|
+
<div className="mb-3">
|
|
958
|
+
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
959
|
+
Info
|
|
960
|
+
</label>
|
|
961
|
+
<input
|
|
962
|
+
type="text"
|
|
963
|
+
value={param.info || ''}
|
|
964
|
+
onChange={(e) => updateParameter(index, 'info', e.target.value)}
|
|
965
|
+
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200"
|
|
966
|
+
placeholder="Partition key, sorting key, indexed column, etc."
|
|
967
|
+
/>
|
|
968
|
+
</div>
|
|
969
|
+
|
|
970
|
+
{param.enum && (
|
|
971
|
+
<div className="mb-3">
|
|
972
|
+
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
973
|
+
Enum Values (comma-separated)
|
|
974
|
+
</label>
|
|
975
|
+
<input
|
|
976
|
+
type="text"
|
|
977
|
+
value={Array.isArray(param.enum) ? param.enum.join(', ') : param.enum}
|
|
978
|
+
onChange={(e) => {
|
|
979
|
+
const enumValues = e.target.value.split(',').map(v => v.trim()).filter(v => v);
|
|
980
|
+
updateParameter(index, 'enum', enumValues.length > 0 ? enumValues : undefined);
|
|
981
|
+
}}
|
|
982
|
+
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200"
|
|
983
|
+
/>
|
|
984
|
+
</div>
|
|
985
|
+
)}
|
|
986
|
+
</div>
|
|
987
|
+
))}
|
|
988
|
+
</div>
|
|
989
|
+
) : (
|
|
990
|
+
<div className="text-center py-8 text-gray-500 dark:text-gray-400">
|
|
991
|
+
<p>No parameters defined. Click "Add Parameter" to add one.</p>
|
|
992
|
+
</div>
|
|
993
|
+
)}
|
|
994
|
+
</div>
|
|
995
|
+
</div>
|
|
996
|
+
</div>
|
|
997
|
+
</div>
|
|
998
|
+
);
|
|
999
|
+
}
|
|
1000
|
+
|