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.
@@ -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
+