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
package/README.md
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# Agentic-Astra Catalog
|
|
2
|
+
|
|
3
|
+
A Next.js frontend application for viewing and editing tools stored in an Astra DB catalog collection.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Dark/Light Mode**: Toggle between dark and light themes
|
|
8
|
+
- **Tool List**: Browse all tools from the catalog collection
|
|
9
|
+
- **Tool Editor**: Edit tool details including parameters, configuration, and metadata
|
|
10
|
+
- **Server-Side API**: All database interactions happen on the server for security
|
|
11
|
+
- **Real-time Updates**: Connect directly to Astra DB using `@datastax/astra-db-ts`
|
|
12
|
+
|
|
13
|
+
## Prerequisites
|
|
14
|
+
|
|
15
|
+
- Node.js 18+ and npm/yarn/pnpm
|
|
16
|
+
- Astra DB credentials (token, endpoint, database name)
|
|
17
|
+
|
|
18
|
+
## Setup
|
|
19
|
+
|
|
20
|
+
1. Install dependencies:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
2. Configure environment variables:
|
|
27
|
+
|
|
28
|
+
Create a `.env.local` file in the `frontend/` directory:
|
|
29
|
+
|
|
30
|
+
```env
|
|
31
|
+
ASTRA_DB_APPLICATION_TOKEN=your_astra_db_token
|
|
32
|
+
ASTRA_DB_API_ENDPOINT=https://your-database-id-your-region.apps.astra.datastax.com
|
|
33
|
+
ASTRA_DB_DB_NAME=your_database_name
|
|
34
|
+
ASTRA_DB_CATALOG_COLLECTION=tool_catalog
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**Note**: Next.js automatically loads environment variables from `.env.local` for local development. These variables are only available on the server side, keeping your credentials secure.
|
|
38
|
+
|
|
39
|
+
## Development
|
|
40
|
+
|
|
41
|
+
Run the development server:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npm run dev
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The app will be available at `http://localhost:3000`.
|
|
48
|
+
|
|
49
|
+
## Building
|
|
50
|
+
|
|
51
|
+
Build for production:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
npm run build
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Start the production server:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
npm start
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Usage
|
|
64
|
+
|
|
65
|
+
### Local Development
|
|
66
|
+
|
|
67
|
+
1. Set up environment variables (see Setup above)
|
|
68
|
+
2. Run `npm run dev`
|
|
69
|
+
3. Open the app in your browser at `http://localhost:3000`
|
|
70
|
+
|
|
71
|
+
## Environment Variables
|
|
72
|
+
|
|
73
|
+
| Variable | Required | Description | Default |
|
|
74
|
+
|----------|----------|-------------|---------|
|
|
75
|
+
| `ASTRA_DB_APPLICATION_TOKEN` | Yes | Astra DB application token | - |
|
|
76
|
+
| `ASTRA_DB_API_ENDPOINT` | Yes | Astra DB API endpoint URL | - |
|
|
77
|
+
| `ASTRA_DB_DB_NAME` | Yes | Name of the Astra DB database | - |
|
|
78
|
+
| `ASTRA_DB_CATALOG_COLLECTION` | No | Name of the catalog collection | `tool_catalog` |
|
|
79
|
+
|
|
80
|
+
## Project Structure
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
frontend/
|
|
84
|
+
├── app/
|
|
85
|
+
│ ├── api/
|
|
86
|
+
│ │ └── tools/
|
|
87
|
+
│ │ └── route.ts # API route for tools (GET, POST)
|
|
88
|
+
│ ├── globals.css # Global styles with Tailwind
|
|
89
|
+
│ ├── layout.tsx # Root layout
|
|
90
|
+
│ └── page.tsx # Main page component
|
|
91
|
+
├── components/
|
|
92
|
+
│ ├── ThemeToggle.tsx # Dark/light mode toggle
|
|
93
|
+
│ ├── ToolList.tsx # Tool list sidebar
|
|
94
|
+
│ └── ToolEditor.tsx # Tool editing form
|
|
95
|
+
├── lib/
|
|
96
|
+
│ └── astraClient.ts # Astra DB client (server-side only)
|
|
97
|
+
├── next.config.js
|
|
98
|
+
├── package.json
|
|
99
|
+
├── tailwind.config.js
|
|
100
|
+
└── tsconfig.json
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Technologies
|
|
104
|
+
|
|
105
|
+
- **Next.js 14**: React framework with App Router
|
|
106
|
+
- **React 18**: UI framework
|
|
107
|
+
- **TypeScript**: Type safety
|
|
108
|
+
- **Tailwind CSS**: Styling with dark mode support
|
|
109
|
+
- **@datastax/astra-db-ts**: Official Astra DB TypeScript client (server-side only)
|
|
110
|
+
|
|
111
|
+
## Architecture
|
|
112
|
+
|
|
113
|
+
This application uses Next.js API routes to handle all database interactions on the server:
|
|
114
|
+
|
|
115
|
+
- **Client-side**: React components make fetch requests to `/api/tools`
|
|
116
|
+
- **Server-side**: API routes in `app/api/tools/route.ts` handle database operations
|
|
117
|
+
- **Security**: Database credentials are never exposed to the client
|
|
118
|
+
|
|
119
|
+
## API Endpoints
|
|
120
|
+
|
|
121
|
+
### GET `/api/tools`
|
|
122
|
+
Fetches all tools from the catalog collection.
|
|
123
|
+
|
|
124
|
+
**Response:**
|
|
125
|
+
```json
|
|
126
|
+
{
|
|
127
|
+
"success": true,
|
|
128
|
+
"tools": [...]
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### POST `/api/tools`
|
|
133
|
+
Creates or updates a tool in the catalog collection.
|
|
134
|
+
|
|
135
|
+
**Request Body:**
|
|
136
|
+
```json
|
|
137
|
+
{
|
|
138
|
+
"_id": "...",
|
|
139
|
+
"name": "...",
|
|
140
|
+
"description": "...",
|
|
141
|
+
...
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
**Response:**
|
|
146
|
+
```json
|
|
147
|
+
{
|
|
148
|
+
"success": true
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## License
|
|
153
|
+
|
|
154
|
+
Same as the parent project.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { getAstraClient } from '@/lib/astraClient';
|
|
3
|
+
|
|
4
|
+
export async function GET(request: NextRequest) {
|
|
5
|
+
try {
|
|
6
|
+
const { searchParams } = new URL(request.url);
|
|
7
|
+
const type = searchParams.get('type'); // 'keyspaces', 'collections', 'tables'
|
|
8
|
+
const dbName = searchParams.get('dbName') || undefined;
|
|
9
|
+
|
|
10
|
+
const client = getAstraClient();
|
|
11
|
+
await client.connect();
|
|
12
|
+
|
|
13
|
+
let result: string[] = [];
|
|
14
|
+
|
|
15
|
+
switch (type) {
|
|
16
|
+
case 'keyspaces':
|
|
17
|
+
result = await client.listKeyspaces();
|
|
18
|
+
break;
|
|
19
|
+
case 'collections':
|
|
20
|
+
result = await client.listCollections(dbName);
|
|
21
|
+
break;
|
|
22
|
+
case 'tables':
|
|
23
|
+
result = await client.listTables(dbName);
|
|
24
|
+
break;
|
|
25
|
+
default:
|
|
26
|
+
return NextResponse.json(
|
|
27
|
+
{ success: false, error: 'Invalid type. Must be "keyspaces", "collections", or "tables"' },
|
|
28
|
+
{ status: 400 }
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return NextResponse.json({ success: true, objects: result });
|
|
33
|
+
} catch (error) {
|
|
34
|
+
console.error('Error listing database objects:', error);
|
|
35
|
+
return NextResponse.json(
|
|
36
|
+
{
|
|
37
|
+
success: false,
|
|
38
|
+
error: error instanceof Error ? error.message : 'Failed to list database objects',
|
|
39
|
+
},
|
|
40
|
+
{ status: 500 }
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { getAstraClient, extractAttributes, Tool } from '@/lib/astraClient';
|
|
3
|
+
|
|
4
|
+
export async function POST(request: NextRequest) {
|
|
5
|
+
try {
|
|
6
|
+
const tool: Tool = await request.json();
|
|
7
|
+
const client = getAstraClient();
|
|
8
|
+
const documents = await client.getSampleDocuments(tool, 5);
|
|
9
|
+
const attributes = extractAttributes(documents);
|
|
10
|
+
|
|
11
|
+
return NextResponse.json({
|
|
12
|
+
success: true,
|
|
13
|
+
attributes,
|
|
14
|
+
sampleCount: documents.length
|
|
15
|
+
});
|
|
16
|
+
} catch (error) {
|
|
17
|
+
console.error('Error fetching attributes:', error);
|
|
18
|
+
return NextResponse.json(
|
|
19
|
+
{
|
|
20
|
+
success: false,
|
|
21
|
+
error: error instanceof Error ? error.message : 'Failed to fetch attributes'
|
|
22
|
+
},
|
|
23
|
+
{ status: 500 }
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { getAstraClient, extractAttributes } from '@/lib/astraClient';
|
|
3
|
+
import OpenAI from 'openai';
|
|
4
|
+
|
|
5
|
+
export async function POST(request: NextRequest) {
|
|
6
|
+
try {
|
|
7
|
+
const { dataType, name, dbName } = await request.json();
|
|
8
|
+
|
|
9
|
+
if (!name || !dataType) {
|
|
10
|
+
return NextResponse.json(
|
|
11
|
+
{ success: false, error: 'Collection/table name and data type are required' },
|
|
12
|
+
{ status: 400 }
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Get sample documents
|
|
17
|
+
const client = getAstraClient();
|
|
18
|
+
await client.connect();
|
|
19
|
+
const tool: any = {
|
|
20
|
+
[dataType === 'collection' ? 'collection_name' : 'table_name']: name,
|
|
21
|
+
db_name: dbName || process.env.ASTRA_DB_DB_NAME || '',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const documents = await client.getSampleDocuments(tool, 10);
|
|
25
|
+
const attributes = extractAttributes(documents);
|
|
26
|
+
|
|
27
|
+
if (documents.length === 0) {
|
|
28
|
+
return NextResponse.json(
|
|
29
|
+
{ success: false, error: `No documents found in ${dataType} "${name}"` },
|
|
30
|
+
{ status: 404 }
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Prepare sample data for OpenAI
|
|
35
|
+
const sampleData = documents.slice(0, 5).map((doc: any) => {
|
|
36
|
+
const { _id, ...rest } = doc;
|
|
37
|
+
return rest;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Initialize OpenAI client
|
|
41
|
+
const openai = new OpenAI({
|
|
42
|
+
apiKey: process.env.OPENAI_API_KEY,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Create prompt for OpenAI
|
|
46
|
+
const prompt = `You are an expert at creating database query tool specifications. Based on the following ${dataType} structure and sample data, generate a comprehensive tool specification in JSON format.
|
|
47
|
+
|
|
48
|
+
${dataType} Name: ${name}
|
|
49
|
+
Available Attributes: ${attributes.join(', ')}
|
|
50
|
+
|
|
51
|
+
Sample Documents (first 5):
|
|
52
|
+
${JSON.stringify(sampleData, null, 2)}
|
|
53
|
+
|
|
54
|
+
Generate a tool specification JSON with the following structure:
|
|
55
|
+
{
|
|
56
|
+
"name": "descriptive_tool_name",
|
|
57
|
+
"description": "Clear description of what this tool does",
|
|
58
|
+
"type": "tool",
|
|
59
|
+
"method": "find",
|
|
60
|
+
"${dataType === 'collection' ? 'collection_name' : 'table_name'}": "${name}",
|
|
61
|
+
"db_name": "${dbName || 'default'}",
|
|
62
|
+
"parameters": [
|
|
63
|
+
{
|
|
64
|
+
"param": "parameter_name",
|
|
65
|
+
"paramMode": "tool_param",
|
|
66
|
+
"type": "string|number|boolean|text|timestamp|float|vector",
|
|
67
|
+
"description": "Parameter description",
|
|
68
|
+
"attribute": "attribute_name_from_list",
|
|
69
|
+
"operator": "$eq|$gt|$gte|$lt|$lte|$in|$ne",
|
|
70
|
+
"required": true|false
|
|
71
|
+
}
|
|
72
|
+
],
|
|
73
|
+
"projection": {
|
|
74
|
+
"attribute_name": 1
|
|
75
|
+
},
|
|
76
|
+
"limit": 10,
|
|
77
|
+
"enabled": true,
|
|
78
|
+
"tags": ["relevant", "tags"]
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
Return ONLY valid JSON, no markdown, no explanations.`;
|
|
82
|
+
|
|
83
|
+
// Call OpenAI
|
|
84
|
+
const completion = await openai.chat.completions.create({
|
|
85
|
+
model: process.env.OPENAI_MODEL || 'gpt-4o-mini',
|
|
86
|
+
messages: [
|
|
87
|
+
{
|
|
88
|
+
role: 'system',
|
|
89
|
+
content: 'You are an expert at creating database query tool specifications. Always return valid JSON only, no markdown formatting.',
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
role: 'user',
|
|
93
|
+
content: prompt,
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
temperature: 0.3,
|
|
97
|
+
response_format: { type: 'json_object' },
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const responseContent = completion.choices[0]?.message?.content;
|
|
101
|
+
if (!responseContent) {
|
|
102
|
+
throw new Error('No response from OpenAI');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Parse the JSON response
|
|
106
|
+
let toolSpec;
|
|
107
|
+
try {
|
|
108
|
+
toolSpec = JSON.parse(responseContent);
|
|
109
|
+
} catch (parseError) {
|
|
110
|
+
// Try to extract JSON from markdown if present
|
|
111
|
+
const jsonMatch = responseContent.match(/```json\s*([\s\S]*?)\s*```/) ||
|
|
112
|
+
responseContent.match(/```\s*([\s\S]*?)\s*```/);
|
|
113
|
+
if (jsonMatch) {
|
|
114
|
+
toolSpec = JSON.parse(jsonMatch[1]);
|
|
115
|
+
} else {
|
|
116
|
+
throw new Error('Failed to parse OpenAI response as JSON');
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Ensure required fields are set
|
|
121
|
+
toolSpec[dataType === 'collection' ? 'collection_name' : 'table_name'] = name;
|
|
122
|
+
toolSpec.db_name = dbName || process.env.ASTRA_DB_DB_NAME || '';
|
|
123
|
+
toolSpec.type = 'tool';
|
|
124
|
+
toolSpec.enabled = toolSpec.enabled !== false;
|
|
125
|
+
|
|
126
|
+
// Normalize parameters
|
|
127
|
+
if (toolSpec.parameters && Array.isArray(toolSpec.parameters)) {
|
|
128
|
+
toolSpec.parameters = toolSpec.parameters.map((param: any) => ({
|
|
129
|
+
...param,
|
|
130
|
+
paramMode: param.paramMode || 'tool_param',
|
|
131
|
+
}));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return NextResponse.json({ success: true, tool: toolSpec });
|
|
135
|
+
} catch (error) {
|
|
136
|
+
console.error('Error generating tool:', error);
|
|
137
|
+
return NextResponse.json(
|
|
138
|
+
{
|
|
139
|
+
success: false,
|
|
140
|
+
error: error instanceof Error ? error.message : 'Failed to generate tool specification',
|
|
141
|
+
},
|
|
142
|
+
{ status: 500 }
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { getAstraClient } from '@/lib/astraClient';
|
|
3
|
+
|
|
4
|
+
export async function GET() {
|
|
5
|
+
try {
|
|
6
|
+
const client = getAstraClient();
|
|
7
|
+
const tools = await client.getTools();
|
|
8
|
+
return NextResponse.json({ success: true, tools });
|
|
9
|
+
} catch (error) {
|
|
10
|
+
console.error('Error fetching tools:', error);
|
|
11
|
+
return NextResponse.json(
|
|
12
|
+
{
|
|
13
|
+
success: false,
|
|
14
|
+
error: error instanceof Error ? error.message : 'Failed to fetch tools'
|
|
15
|
+
},
|
|
16
|
+
{ status: 500 }
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function POST(request: NextRequest) {
|
|
22
|
+
try {
|
|
23
|
+
const tool = await request.json();
|
|
24
|
+
const client = getAstraClient();
|
|
25
|
+
await client.updateTool(tool);
|
|
26
|
+
return NextResponse.json({ success: true });
|
|
27
|
+
} catch (error) {
|
|
28
|
+
console.error('Error updating tool:', error);
|
|
29
|
+
return NextResponse.json(
|
|
30
|
+
{
|
|
31
|
+
success: false,
|
|
32
|
+
error: error instanceof Error ? error.message : 'Failed to update tool'
|
|
33
|
+
},
|
|
34
|
+
{ status: 500 }
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
package/app/globals.css
ADDED
package/app/layout.tsx
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Metadata } from 'next'
|
|
2
|
+
import './globals.css'
|
|
3
|
+
|
|
4
|
+
export const metadata: Metadata = {
|
|
5
|
+
title: 'Agentic Astra Catalog',
|
|
6
|
+
description: 'Manage your Astra DB tool catalog',
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default function RootLayout({
|
|
10
|
+
children,
|
|
11
|
+
}: {
|
|
12
|
+
children: React.ReactNode
|
|
13
|
+
}) {
|
|
14
|
+
return (
|
|
15
|
+
<html lang="en">
|
|
16
|
+
<body>{children}</body>
|
|
17
|
+
</html>
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
package/app/page.tsx
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from 'react';
|
|
4
|
+
import ThemeToggle from '@/components/ThemeToggle';
|
|
5
|
+
import ToolList from '@/components/ToolList';
|
|
6
|
+
import ToolEditor from '@/components/ToolEditor';
|
|
7
|
+
import { Tool } from '@/lib/astraClient';
|
|
8
|
+
|
|
9
|
+
export default function Home() {
|
|
10
|
+
const [tools, setTools] = useState<Tool[]>([]);
|
|
11
|
+
const [selectedTool, setSelectedTool] = useState<Tool | null>(null);
|
|
12
|
+
const [loading, setLoading] = useState(true);
|
|
13
|
+
const [error, setError] = useState<string | null>(null);
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
loadTools();
|
|
17
|
+
}, []);
|
|
18
|
+
|
|
19
|
+
const loadTools = async (showLoading = true) => {
|
|
20
|
+
try {
|
|
21
|
+
if (showLoading) {
|
|
22
|
+
setLoading(true);
|
|
23
|
+
}
|
|
24
|
+
setError(null);
|
|
25
|
+
const response = await fetch('/api/tools');
|
|
26
|
+
const data = await response.json();
|
|
27
|
+
|
|
28
|
+
if (!response.ok || !data.success) {
|
|
29
|
+
throw new Error(data.error || 'Failed to load tools');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const loadedTools = data.tools || [];
|
|
33
|
+
// Sort tools alphabetically by name (case-insensitive)
|
|
34
|
+
const sortedTools = loadedTools.sort((a: Tool, b: Tool) => {
|
|
35
|
+
const nameA = (a.name || '').toLowerCase();
|
|
36
|
+
const nameB = (b.name || '').toLowerCase();
|
|
37
|
+
return nameA.localeCompare(nameB);
|
|
38
|
+
});
|
|
39
|
+
setTools(sortedTools);
|
|
40
|
+
|
|
41
|
+
// Update selected tool if it still exists
|
|
42
|
+
if (selectedTool) {
|
|
43
|
+
const updatedTool = sortedTools.find(
|
|
44
|
+
(t: Tool) => (t._id && t._id === selectedTool._id) || t.name === selectedTool.name
|
|
45
|
+
);
|
|
46
|
+
if (updatedTool) {
|
|
47
|
+
setSelectedTool(updatedTool);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
} catch (err) {
|
|
51
|
+
setError(err instanceof Error ? err.message : 'Failed to load tools');
|
|
52
|
+
console.error('Error loading tools:', err);
|
|
53
|
+
} finally {
|
|
54
|
+
if (showLoading) {
|
|
55
|
+
setLoading(false);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const handleToolSave = async (savedTool: Tool) => {
|
|
61
|
+
// Refresh the tools list without showing loading screen
|
|
62
|
+
await loadTools(false);
|
|
63
|
+
// Update selected tool with the saved data
|
|
64
|
+
setSelectedTool(savedTool);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
if (loading) {
|
|
68
|
+
return (
|
|
69
|
+
<div className="h-screen flex items-center justify-center bg-white dark:bg-gray-800">
|
|
70
|
+
<div className="text-center">
|
|
71
|
+
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
|
72
|
+
<p className="text-gray-600 dark:text-gray-400">Loading tools...</p>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (error) {
|
|
79
|
+
return (
|
|
80
|
+
<div className="h-screen flex items-center justify-center bg-white dark:bg-gray-800">
|
|
81
|
+
<div className="text-center max-w-md mx-auto p-6">
|
|
82
|
+
<div className="text-red-600 dark:text-red-400 mb-4">
|
|
83
|
+
<svg className="w-16 h-16 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
84
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
85
|
+
</svg>
|
|
86
|
+
</div>
|
|
87
|
+
<h2 className="text-xl font-semibold text-gray-800 dark:text-gray-200 mb-2">Error Loading Tools</h2>
|
|
88
|
+
<p className="text-gray-600 dark:text-gray-400 mb-4">{error}</p>
|
|
89
|
+
<button
|
|
90
|
+
onClick={() => loadTools()}
|
|
91
|
+
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
|
|
92
|
+
>
|
|
93
|
+
Retry
|
|
94
|
+
</button>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const handleNewTool = () => {
|
|
101
|
+
setSelectedTool({
|
|
102
|
+
type: 'tool',
|
|
103
|
+
name: '',
|
|
104
|
+
collection_name: '',
|
|
105
|
+
table_name: '',
|
|
106
|
+
db_name: '',
|
|
107
|
+
enabled: true,
|
|
108
|
+
} as Tool);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<div className="h-screen flex bg-white dark:bg-gray-800">
|
|
113
|
+
<ThemeToggle />
|
|
114
|
+
<ToolList tools={tools} selectedTool={selectedTool} onSelectTool={setSelectedTool} onNewTool={handleNewTool} />
|
|
115
|
+
<ToolEditor tool={selectedTool} onSave={handleToolSave} />
|
|
116
|
+
</div>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CLI entry point for agentic-astra-catalog
|
|
5
|
+
* This script runs the Next.js development server
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { spawn } = require('child_process');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
|
|
12
|
+
// Get the directory where the package is installed
|
|
13
|
+
const packageDir = path.resolve(__dirname, '..');
|
|
14
|
+
|
|
15
|
+
// Check if node_modules exists, if not, install dependencies
|
|
16
|
+
const nodeModulesPath = path.join(packageDir, 'node_modules');
|
|
17
|
+
const nextPath = path.join(nodeModulesPath, '.bin', 'next');
|
|
18
|
+
|
|
19
|
+
function runNextDev() {
|
|
20
|
+
const port = process.env.PORT || '3000';
|
|
21
|
+
const hostname = process.env.HOSTNAME || 'localhost';
|
|
22
|
+
|
|
23
|
+
console.log(`🚀 Starting Agentic Astra Catalog...`);
|
|
24
|
+
console.log(`📦 Package directory: ${packageDir}`);
|
|
25
|
+
console.log(`🌐 Server will be available at http://${hostname}:${port}`);
|
|
26
|
+
console.log(`\n💡 Make sure to set up your .env.local file with Astra DB credentials!\n`);
|
|
27
|
+
|
|
28
|
+
// Spawn next dev process
|
|
29
|
+
const nextProcess = spawn('npx', ['next', 'dev', '-p', port, '-H', hostname], {
|
|
30
|
+
cwd: packageDir,
|
|
31
|
+
stdio: 'inherit',
|
|
32
|
+
shell: true
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
nextProcess.on('error', (error) => {
|
|
36
|
+
console.error('❌ Error starting server:', error.message);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
nextProcess.on('exit', (code) => {
|
|
41
|
+
process.exit(code || 0);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Handle graceful shutdown
|
|
45
|
+
process.on('SIGINT', () => {
|
|
46
|
+
console.log('\n\n👋 Shutting down...');
|
|
47
|
+
nextProcess.kill();
|
|
48
|
+
process.exit(0);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
process.on('SIGTERM', () => {
|
|
52
|
+
nextProcess.kill();
|
|
53
|
+
process.exit(0);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Check if dependencies are installed
|
|
58
|
+
if (!fs.existsSync(nodeModulesPath) || !fs.existsSync(nextPath)) {
|
|
59
|
+
console.log('📦 Installing dependencies...');
|
|
60
|
+
const installProcess = spawn('npm', ['install'], {
|
|
61
|
+
cwd: packageDir,
|
|
62
|
+
stdio: 'inherit',
|
|
63
|
+
shell: true
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
installProcess.on('exit', (code) => {
|
|
67
|
+
if (code === 0) {
|
|
68
|
+
runNextDev();
|
|
69
|
+
} else {
|
|
70
|
+
console.error('❌ Failed to install dependencies');
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
} else {
|
|
75
|
+
runNextDev();
|
|
76
|
+
}
|
|
77
|
+
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
|
|
5
|
+
export default function ThemeToggle() {
|
|
6
|
+
const [isDark, setIsDark] = useState(() => {
|
|
7
|
+
if (typeof window !== 'undefined') {
|
|
8
|
+
const stored = localStorage.getItem('theme');
|
|
9
|
+
if (stored) {
|
|
10
|
+
return stored === 'dark';
|
|
11
|
+
}
|
|
12
|
+
return window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
13
|
+
}
|
|
14
|
+
return false;
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (isDark) {
|
|
19
|
+
document.documentElement.classList.add('dark');
|
|
20
|
+
localStorage.setItem('theme', 'dark');
|
|
21
|
+
} else {
|
|
22
|
+
document.documentElement.classList.remove('dark');
|
|
23
|
+
localStorage.setItem('theme', 'light');
|
|
24
|
+
}
|
|
25
|
+
}, [isDark]);
|
|
26
|
+
|
|
27
|
+
const toggleTheme = () => {
|
|
28
|
+
setIsDark(!isDark);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<button
|
|
33
|
+
onClick={toggleTheme}
|
|
34
|
+
className="fixed top-4 right-4 p-2 rounded-lg bg-gray-200 dark:bg-gray-800 text-gray-800 dark:text-gray-200 hover:bg-gray-300 dark:hover:bg-gray-700 transition-colors"
|
|
35
|
+
aria-label="Toggle theme"
|
|
36
|
+
>
|
|
37
|
+
{isDark ? (
|
|
38
|
+
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
39
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
|
|
40
|
+
</svg>
|
|
41
|
+
) : (
|
|
42
|
+
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
43
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
|
|
44
|
+
</svg>
|
|
45
|
+
)}
|
|
46
|
+
</button>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|