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 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
+
@@ -0,0 +1,4 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
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
+