diff --git a/apps/web/src/app/api/agents/[uid]/conversation/[conversationId]/route.ts b/apps/web/src/app/api/agents/[id]/conversation/[conversationId]/route.ts similarity index 88% rename from apps/web/src/app/api/agents/[uid]/conversation/[conversationId]/route.ts rename to apps/web/src/app/api/agents/[id]/conversation/[conversationId]/route.ts index 5066f8d..dd4dd74 100644 --- a/apps/web/src/app/api/agents/[uid]/conversation/[conversationId]/route.ts +++ b/apps/web/src/app/api/agents/[id]/conversation/[conversationId]/route.ts @@ -18,7 +18,7 @@ export const dynamic = 'force-dynamic'; export const maxDuration = 300; // 5 minutes for long agentic runs type RouteContext = { - params: Promise<{ uid: string; conversationId: string }>; + params: Promise<{ id: string; conversationId: string }>; }; /** @@ -56,11 +56,12 @@ async function getProviderModel( } /** - * POST /api/agents/[uid]/conversation/[conversationId] + * POST /api/agents/[id]/conversation/[conversationId] * Send a message and stream the AI response via SSE + * Accepts either agent id (cuid) or uid */ export async function POST(request: NextRequest, context: RouteContext): Promise { - const { uid, conversationId } = await context.params; + const { id: idOrUid, conversationId } = await context.params; try { const body = await request.json(); @@ -72,20 +73,39 @@ export async function POST(request: NextRequest, context: RouteContext): Promise ); } - // Fetch agent with all tool relations - const { fetchAgentByUidWithTools, buildAgentTools } = await import('@/lib/agents/build-tools'); - const agent = await fetchAgentByUidWithTools(uid); + // Fetch agent with all tool relations (accepts id or uid) + const { fetchAgentByIdOrUidWithTools, buildAgentTools } = await import( + '@/lib/agents/build-tools' + ); + const agent = await fetchAgentByIdOrUidWithTools(idOrUid); if (!agent) { return NextResponse.json({ success: false, error: 'Agent not found' }, { status: 404 }); } + // Map provider to expected key name format + const providerKeyNames: Record = { + OPENAI: 'OPENAI_API_KEY', + ANTHROPIC: 'ANTHROPIC_API_KEY', + GOOGLE: 'GOOGLE_API_KEY', + GROQ: 'GROQ_API_KEY', + MISTRAL: 'MISTRAL_API_KEY', + }; + const keyName = providerKeyNames[agent.provider]; + + if (!keyName) { + return NextResponse.json( + { success: false, error: `Unsupported provider: ${agent.provider}` }, + { status: 400 } + ); + } + // Get user's API key for this provider const userApiKey = await prisma.userApiKey.findUnique({ where: { - userId_provider: { + userId_keyName: { userId: agent.userId, - provider: agent.provider, + keyName, }, }, }); @@ -342,16 +362,18 @@ export async function POST(request: NextRequest, context: RouteContext): Promise } /** - * GET /api/agents/[uid]/conversation/[conversationId] - * Retrieve conversation history + * GET /api/agents/[id]/conversation/[conversationId] + * Retrieve conversation history (accepts id or uid) */ export async function GET(_request: NextRequest, context: RouteContext): Promise { - const { uid, conversationId } = await context.params; + const { id: idOrUid, conversationId } = await context.params; try { - // Fetch agent - const agent = await prisma.agent.findUnique({ - where: { uid }, + // Fetch agent by id or uid + const agent = await prisma.agent.findFirst({ + where: { + OR: [{ id: idOrUid }, { uid: idOrUid }], + }, select: { id: true }, }); @@ -413,16 +435,18 @@ export async function GET(_request: NextRequest, context: RouteContext): Promise } /** - * DELETE /api/agents/[uid]/conversation/[conversationId] - * Delete a conversation + * DELETE /api/agents/[id]/conversation/[conversationId] + * Delete a conversation (accepts id or uid) */ export async function DELETE(_request: NextRequest, context: RouteContext): Promise { - const { uid, conversationId } = await context.params; + const { id: idOrUid, conversationId } = await context.params; try { - // Fetch agent - const agent = await prisma.agent.findUnique({ - where: { uid }, + // Fetch agent by id or uid + const agent = await prisma.agent.findFirst({ + where: { + OR: [{ id: idOrUid }, { uid: idOrUid }], + }, select: { id: true }, }); diff --git a/apps/web/src/app/api/agents/[uid]/conversations/route.ts b/apps/web/src/app/api/agents/[id]/conversations/route.ts similarity index 84% rename from apps/web/src/app/api/agents/[uid]/conversations/route.ts rename to apps/web/src/app/api/agents/[id]/conversations/route.ts index df19fad..3e3e5f0 100644 --- a/apps/web/src/app/api/agents/[uid]/conversations/route.ts +++ b/apps/web/src/app/api/agents/[id]/conversations/route.ts @@ -12,24 +12,26 @@ export const dynamic = 'force-dynamic'; export const maxDuration = 30; type RouteContext = { - params: Promise<{ uid: string }>; + params: Promise<{ id: string }>; }; /** - * GET /api/agents/[uid]/conversations - * List all conversations for an agent + * GET /api/agents/[id]/conversations + * List all conversations for an agent (accepts id or uid) */ export async function GET(request: NextRequest, context: RouteContext): Promise { - const { uid } = await context.params; + const { id: idOrUid } = await context.params; const { searchParams } = new URL(request.url); const limit = Math.min(Number.parseInt(searchParams.get('limit') || '20', 10), 100); const offset = Number.parseInt(searchParams.get('offset') || '0', 10); try { - // Fetch agent - const agent = await prisma.agent.findUnique({ - where: { uid }, + // Fetch agent by id or uid + const agent = await prisma.agent.findFirst({ + where: { + OR: [{ id: idOrUid }, { uid: idOrUid }], + }, select: { id: true }, }); diff --git a/apps/web/src/app/api/user/api-keys/[provider]/route.ts b/apps/web/src/app/api/user/api-keys/[provider]/route.ts index 8647ac5..e641253 100644 --- a/apps/web/src/app/api/user/api-keys/[provider]/route.ts +++ b/apps/web/src/app/api/user/api-keys/[provider]/route.ts @@ -1,5 +1,3 @@ -import type { AIProvider } from '@prisma/client'; - import { prisma } from '@tpmjs/db'; import { headers } from 'next/headers'; import { type NextRequest, NextResponse } from 'next/server'; @@ -14,8 +12,8 @@ type RouteContext = { }; /** - * DELETE /api/user/api-keys/[provider] - * Remove an API key for a provider + * DELETE /api/user/api-keys/[keyName] + * Remove an API key by name */ export async function DELETE(_request: NextRequest, context: RouteContext): Promise { try { @@ -24,18 +22,12 @@ export async function DELETE(_request: NextRequest, context: RouteContext): Prom return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 }); } - const { provider } = await context.params; - - // Validate provider is a valid enum value - const validProviders = ['OPENAI', 'ANTHROPIC', 'GOOGLE', 'GROQ', 'MISTRAL']; - if (!validProviders.includes(provider.toUpperCase())) { - return NextResponse.json({ success: false, error: 'Invalid provider' }, { status: 400 }); - } + const { provider: keyName } = await context.params; await prisma.userApiKey.deleteMany({ where: { userId: session.user.id, - provider: provider.toUpperCase() as AIProvider, + keyName, }, }); diff --git a/apps/web/src/app/api/user/api-keys/route.ts b/apps/web/src/app/api/user/api-keys/route.ts index a1318af..f95dace 100644 --- a/apps/web/src/app/api/user/api-keys/route.ts +++ b/apps/web/src/app/api/user/api-keys/route.ts @@ -1,7 +1,7 @@ import { prisma } from '@tpmjs/db'; -import { AddApiKeySchema } from '@tpmjs/types/agent'; import { headers } from 'next/headers'; import { type NextRequest, NextResponse } from 'next/server'; +import { z } from 'zod'; import { auth } from '~/lib/auth'; import { encryptApiKey, getKeyHint } from '~/lib/crypto/api-keys'; @@ -9,6 +9,11 @@ import { encryptApiKey, getKeyHint } from '~/lib/crypto/api-keys'; export const runtime = 'nodejs'; export const dynamic = 'force-dynamic'; +const AddKeySchema = z.object({ + keyName: z.string().min(1).max(100), + keyValue: z.string().min(1), +}); + /** * GET /api/user/api-keys * List user's stored API keys (masked) @@ -23,12 +28,13 @@ export async function GET(): Promise { const apiKeys = await prisma.userApiKey.findMany({ where: { userId: session.user.id }, select: { - provider: true, + id: true, + keyName: true, keyHint: true, createdAt: true, updatedAt: true, }, - orderBy: { createdAt: 'asc' }, + orderBy: { keyName: 'asc' }, }); return NextResponse.json({ @@ -43,141 +49,72 @@ export async function GET(): Promise { /** * POST /api/user/api-keys - * Add or update an API key for a provider + * Add or update an API key */ export async function POST(request: NextRequest): Promise { - const startTime = Date.now(); - console.log('[api-keys] POST request received'); - try { - // Step 1: Auth check - console.log('[api-keys] Checking session...'); const session = await auth.api.getSession({ headers: await headers() }); if (!session?.user?.id) { - console.log('[api-keys] Unauthorized - no session or user id'); return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 }); } - console.log('[api-keys] Session valid for user:', session.user.id); - // Step 2: Parse request body - console.log('[api-keys] Parsing request body...'); - let body: unknown; - try { - body = await request.json(); - console.log('[api-keys] Body parsed, provider:', (body as { provider?: string })?.provider); - } catch (parseError) { - console.error('[api-keys] Failed to parse JSON body:', parseError); - return NextResponse.json( - { success: false, error: 'Invalid JSON body' }, - { status: 400 } - ); - } - - // Step 3: Validate schema - console.log('[api-keys] Validating schema...'); - const parsed = AddApiKeySchema.safeParse(body); + const body = await request.json(); + const parsed = AddKeySchema.safeParse(body); if (!parsed.success) { - console.error('[api-keys] Schema validation failed:', parsed.error.flatten()); return NextResponse.json( { success: false, error: 'Invalid request', details: parsed.error.flatten() }, { status: 400 } ); } - console.log('[api-keys] Schema valid, provider:', parsed.data.provider); - const { provider, apiKey } = parsed.data; - console.log('[api-keys] API key length:', apiKey.length, 'chars'); + const { keyName, keyValue } = parsed.data; - // Step 4: Encrypt the key - console.log('[api-keys] Encrypting API key...'); - let encrypted: string; - let iv: string; - try { - const hasEncryptionSecret = !!process.env.API_KEY_ENCRYPTION_SECRET; - console.log('[api-keys] API_KEY_ENCRYPTION_SECRET present:', hasEncryptionSecret); - if (!hasEncryptionSecret) { - console.error('[api-keys] CRITICAL: API_KEY_ENCRYPTION_SECRET is not set!'); - return NextResponse.json( - { success: false, error: 'Server configuration error: encryption not configured' }, - { status: 500 } - ); - } - const result = encryptApiKey(apiKey); - encrypted = result.encrypted; - iv = result.iv; - console.log('[api-keys] Encryption successful, encrypted length:', encrypted.length); - } catch (encryptError) { - console.error('[api-keys] Encryption failed:', encryptError); + // Check encryption secret + if (!process.env.API_KEY_ENCRYPTION_SECRET) { + console.error('[api-keys] API_KEY_ENCRYPTION_SECRET not set'); return NextResponse.json( - { success: false, error: 'Failed to encrypt API key' }, + { success: false, error: 'Server configuration error' }, { status: 500 } ); } - const keyHint = getKeyHint(apiKey); - console.log('[api-keys] Key hint:', keyHint); + const { encrypted, iv } = encryptApiKey(keyValue); + const keyHint = getKeyHint(keyValue); - // Step 5: Upsert to database - console.log('[api-keys] Upserting to database...'); - console.log('[api-keys] Where clause: userId_provider =', { userId: session.user.id, provider }); - - let result; - try { - result = await prisma.userApiKey.upsert({ - where: { - userId_provider: { - userId: session.user.id, - provider, - }, - }, - create: { + const result = await prisma.userApiKey.upsert({ + where: { + userId_keyName: { userId: session.user.id, - provider, - encryptedKey: encrypted, - keyIv: iv, - keyHint, + keyName, }, - update: { - encryptedKey: encrypted, - keyIv: iv, - keyHint, - }, - select: { - provider: true, - keyHint: true, - createdAt: true, - updatedAt: true, - }, - }); - console.log('[api-keys] Upsert successful:', result.provider, 'updated at', result.updatedAt); - } catch (dbError) { - console.error('[api-keys] Database upsert failed:', dbError); - console.error('[api-keys] Database error details:', { - name: (dbError as Error).name, - message: (dbError as Error).message, - stack: (dbError as Error).stack?.slice(0, 500), - }); - return NextResponse.json( - { success: false, error: 'Failed to save API key to database' }, - { status: 500 } - ); - } - - const duration = Date.now() - startTime; - console.log(`[api-keys] POST completed successfully in ${duration}ms`); + }, + create: { + userId: session.user.id, + keyName, + encryptedKey: encrypted, + keyIv: iv, + keyHint, + }, + update: { + encryptedKey: encrypted, + keyIv: iv, + keyHint, + }, + select: { + id: true, + keyName: true, + keyHint: true, + createdAt: true, + updatedAt: true, + }, + }); return NextResponse.json({ success: true, data: result, }); } catch (error) { - const duration = Date.now() - startTime; - console.error(`[api-keys] Unexpected error after ${duration}ms:`, error); - console.error('[api-keys] Error details:', { - name: (error as Error).name, - message: (error as Error).message, - stack: (error as Error).stack?.slice(0, 500), - }); + console.error('Failed to save API key:', error); return NextResponse.json({ success: false, error: 'Failed to save API key' }, { status: 500 }); } } diff --git a/apps/web/src/app/dashboard/collections/[id]/page.tsx b/apps/web/src/app/dashboard/collections/[id]/page.tsx index 5337279..6abd65d 100644 --- a/apps/web/src/app/dashboard/collections/[id]/page.tsx +++ b/apps/web/src/app/dashboard/collections/[id]/page.tsx @@ -11,6 +11,130 @@ import { AddToolSearch } from '~/components/collections/AddToolSearch'; import { CollectionForm } from '~/components/collections/CollectionForm'; import { CollectionToolList } from '~/components/collections/CollectionToolList'; +function McpUrlSection({ collectionId }: { collectionId: string }) { + const [copiedUrl, setCopiedUrl] = useState<'http' | 'sse' | null>(null); + const [showConfig, setShowConfig] = useState(false); + + const baseUrl = typeof window !== 'undefined' ? window.location.origin : 'https://tpmjs.com'; + const httpUrl = `${baseUrl}/api/collections/${collectionId}/mcp/http`; + const sseUrl = `${baseUrl}/api/collections/${collectionId}/mcp/sse`; + + const copyToClipboard = async (url: string, type: 'http' | 'sse') => { + await navigator.clipboard.writeText(url); + setCopiedUrl(type); + setTimeout(() => setCopiedUrl(null), 2000); + }; + + const configSnippet = `{ + "mcpServers": { + "tpmjs-collection": { + "command": "npx", + "args": [ + "mcp-remote", + "${httpUrl}" + ] + } + } +}`; + + return ( +
+
+
+ +
+

MCP Server URLs

+ Public +
+ +
+ {/* HTTP Transport */} +
+
+ HTTP Transport + (recommended) +
+
+
+ {httpUrl} +
+ +
+
+ + {/* SSE Transport */} +
+
+ SSE Transport + (streaming) +
+
+
+ {sseUrl} +
+ +
+
+
+ + {/* Config snippet toggle */} +
+ + + {showConfig && ( +
+
+              {configSnippet}
+            
+ +
+ )} +
+ +

+ Use these URLs with{' '} + + Claude Desktop, Cursor, or any MCP client + +

+
+ ); +} + interface CollectionTool { id: string; toolId: string; @@ -334,6 +458,9 @@ export default function CollectionDetailPage(): React.ReactElement { Updated {new Date(collection.updatedAt).toLocaleDateString()} + {/* MCP URLs - only for public collections */} + {collection.isPublic && } + {/* Add Tool Search */} {collection.isOwner && (
diff --git a/apps/web/src/app/dashboard/settings/api-keys/page.tsx b/apps/web/src/app/dashboard/settings/api-keys/page.tsx index 904287b..253d3b2 100644 --- a/apps/web/src/app/dashboard/settings/api-keys/page.tsx +++ b/apps/web/src/app/dashboard/settings/api-keys/page.tsx @@ -1,7 +1,5 @@ 'use client'; -import type { AIProvider } from '@tpmjs/types/agent'; -import { SUPPORTED_PROVIDERS } from '@tpmjs/types/agent'; import { Button } from '@tpmjs/ui/Button/Button'; import { Icon } from '@tpmjs/ui/Icon/Icon'; import Link from 'next/link'; @@ -10,161 +8,117 @@ import { useCallback, useEffect, useState } from 'react'; import { AppHeader } from '~/components/AppHeader'; interface ApiKeyInfo { - provider: AIProvider; + id: string; + keyName: string; keyHint: string | null; createdAt: string; updatedAt: string; } -const PROVIDER_NAMES: Record = { - OPENAI: 'OpenAI', - ANTHROPIC: 'Anthropic', - GOOGLE: 'Google', - GROQ: 'Groq', - MISTRAL: 'Mistral', -}; - -const ENV_VAR_MAP: Record = { - OPENAI_API_KEY: 'OPENAI', - ANTHROPIC_API_KEY: 'ANTHROPIC', - GOOGLE_API_KEY: 'GOOGLE', - GOOGLE_GENERATIVE_AI_API_KEY: 'GOOGLE', - GROQ_API_KEY: 'GROQ', - MISTRAL_API_KEY: 'MISTRAL', -}; - export default function ApiKeysPage(): React.ReactElement { const router = useRouter(); - const [apiKeys, setApiKeys] = useState([]); + const [keys, setKeys] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); - // Individual key inputs - const [keyInputs, setKeyInputs] = useState>({ - OPENAI: '', - ANTHROPIC: '', - GOOGLE: '', - GROQ: '', - MISTRAL: '', - }); - const [savingProvider, setSavingProvider] = useState(null); - const [savedProvider, setSavedProvider] = useState(null); - const [deletingProvider, setDeletingProvider] = useState(null); + // Add new key form + const [newKeyName, setNewKeyName] = useState(''); + const [newKeyValue, setNewKeyValue] = useState(''); + const [saving, setSaving] = useState(false); const [saveError, setSaveError] = useState(null); // .env import - const [showEnvImport, setShowEnvImport] = useState(false); + const [showImport, setShowImport] = useState(false); const [envText, setEnvText] = useState(''); const [importing, setImporting] = useState(false); - const fetchApiKeys = useCallback(async () => { + const [deletingId, setDeletingId] = useState(null); + + const fetchKeys = useCallback(async () => { try { const response = await fetch('/api/user/api-keys'); const data = await response.json(); if (data.success) { - setApiKeys(data.data); + setKeys(data.data); } else { if (response.status === 401) { router.push('/sign-in'); return; } - setError(data.error || 'Failed to fetch API keys'); + setError(data.error || 'Failed to fetch keys'); } } catch (err) { - console.error('Failed to fetch API keys:', err); - setError('Failed to fetch API keys'); + console.error('Failed to fetch keys:', err); + setError('Failed to fetch keys'); } finally { setIsLoading(false); } }, [router]); useEffect(() => { - fetchApiKeys(); - }, [fetchApiKeys]); + fetchKeys(); + }, [fetchKeys]); - const saveKey = useCallback(async (provider: AIProvider, apiKey: string) => { - setSavingProvider(provider); - setSavedProvider(null); - setSaveError(null); - - try { - console.log('[saveKey] Saving key for provider:', provider, 'key length:', apiKey.length); - const response = await fetch('/api/user/api-keys', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ provider, apiKey }), - }); - - const result = await response.json(); - console.log('[saveKey] Response:', result); - - if (result.success) { - setApiKeys((prev) => { - const idx = prev.findIndex((k) => k.provider === provider); - if (idx >= 0) { - const updated = [...prev]; - updated[idx] = result.data; - return updated; - } - return [...prev, result.data]; - }); - setKeyInputs((prev) => ({ ...prev, [provider]: '' })); - setSavedProvider(provider); - setTimeout(() => setSavedProvider(null), 2000); - return true; - } else { - console.error('[saveKey] Error:', result.error); - setSaveError(result.error || 'Failed to save'); - return false; - } - } catch (err) { - console.error('[saveKey] Network error:', err); - setSaveError('Network error'); - return false; - } finally { - setSavingProvider(null); - } + const saveKey = useCallback(async (keyName: string, keyValue: string) => { + const response = await fetch('/api/user/api-keys', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ keyName, keyValue }), + }); + const result = await response.json(); + return result; }, []); - const handleSave = useCallback((provider: AIProvider) => { - const value = keyInputs[provider]?.trim(); - if (value) { - saveKey(provider, value); + const handleAddKey = useCallback(async () => { + if (!newKeyName.trim() || !newKeyValue.trim()) return; + + setSaving(true); + setSaveError(null); + + const result = await saveKey(newKeyName.trim(), newKeyValue.trim()); + + if (result.success) { + setNewKeyName(''); + setNewKeyValue(''); + fetchKeys(); + } else { + setSaveError(result.error || 'Failed to save'); } - }, [keyInputs, saveKey]); - const handleDelete = useCallback(async (provider: AIProvider) => { - if (!confirm(`Delete ${PROVIDER_NAMES[provider]} API key?`)) return; + setSaving(false); + }, [newKeyName, newKeyValue, saveKey, fetchKeys]); - setDeletingProvider(provider); + const handleDelete = useCallback(async (keyName: string, id: string) => { + if (!confirm(`Delete ${keyName}?`)) return; + + setDeletingId(id); try { - const response = await fetch(`/api/user/api-keys/${provider}`, { method: 'DELETE' }); + const response = await fetch(`/api/user/api-keys/${encodeURIComponent(keyName)}`, { + method: 'DELETE', + }); const result = await response.json(); if (result.success) { - setApiKeys((prev) => prev.filter((k) => k.provider !== provider)); + setKeys((prev) => prev.filter((k) => k.id !== id)); } } catch (err) { console.error('Failed to delete:', err); } finally { - setDeletingProvider(null); + setDeletingId(null); } }, []); - const handleEnvImport = useCallback(async () => { + const handleImport = useCallback(async () => { const lines = envText.split('\n'); - const keysToSave: { provider: AIProvider; apiKey: string }[] = []; + const keysToSave: { keyName: string; keyValue: string }[] = []; for (const line of lines) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith('#')) continue; - const match = trimmed.match(/^([A-Z_]+)\s*=\s*["']?([^"'\n]+)["']?$/); + const match = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*["']?(.+?)["']?$/); if (match) { - const [, envVar, value] = match; - if (envVar && value) { - const provider = ENV_VAR_MAP[envVar]; - if (provider) { - keysToSave.push({ provider, apiKey: value.trim() }); - } + const [, keyName, keyValue] = match; + if (keyName && keyValue) { + keysToSave.push({ keyName, keyValue: keyValue.trim() }); } } } @@ -172,16 +126,14 @@ export default function ApiKeysPage(): React.ReactElement { if (keysToSave.length === 0) return; setImporting(true); - for (const { provider, apiKey } of keysToSave) { - await saveKey(provider, apiKey); + for (const { keyName, keyValue } of keysToSave) { + await saveKey(keyName, keyValue); } setImporting(false); setEnvText(''); - setShowEnvImport(false); - }, [envText, saveKey]); - - const hasKey = (provider: AIProvider) => apiKeys.some((k) => k.provider === provider); - const getKeyHint = (provider: AIProvider) => apiKeys.find((k) => k.provider === provider)?.keyHint; + setShowImport(false); + fetchKeys(); + }, [envText, saveKey, fetchKeys]); if (isLoading) { return ( @@ -191,7 +143,6 @@ export default function ApiKeysPage(): React.ReactElement {
-
@@ -204,7 +155,7 @@ export default function ApiKeysPage(): React.ReactElement {

{error}

- +
); @@ -219,106 +170,98 @@ export default function ApiKeysPage(): React.ReactElement { -

API Keys

+

Environment Variables

- {/* .env Import */} - {showEnvImport && ( + {/* Import from .env */} + {showImport && (