| | import React, { useState, useEffect } from "react"; |
| | import { motion, AnimatePresence } from "framer-motion"; |
| | import { |
| | Key, |
| | Plus, |
| | Trash2, |
| | Copy, |
| | Check, |
| | Eye, |
| | EyeOff, |
| | Code, |
| | FileText, |
| | AlertCircle, |
| | Clock, |
| | Sparkles, |
| | ExternalLink, |
| | } from "lucide-react"; |
| | import { Button } from "@/components/ui/button"; |
| | import { Input } from "@/components/ui/input"; |
| | import { Badge } from "@/components/ui/badge"; |
| | import { cn } from "@/lib/utils"; |
| | import { createAPIKey, listAPIKeys, deleteAPIKey } from "@/services/api"; |
| |
|
| | const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || "https://seth0330-ezofisocr.hf.space"; |
| |
|
| | export default function APIKeys() { |
| | const [apiKeys, setApiKeys] = useState([]); |
| | const [isLoading, setIsLoading] = useState(true); |
| | const [error, setError] = useState(null); |
| | const [showCreateModal, setShowCreateModal] = useState(false); |
| | const [newKeyName, setNewKeyName] = useState(""); |
| | const [isCreating, setIsCreating] = useState(false); |
| | const [newlyCreatedKey, setNewlyCreatedKey] = useState(null); |
| | const [copiedKeyId, setCopiedKeyId] = useState(null); |
| | const [visibleKeys, setVisibleKeys] = useState(new Set()); |
| |
|
| | |
| | useEffect(() => { |
| | fetchAPIKeys(); |
| | }, []); |
| |
|
| | const fetchAPIKeys = async () => { |
| | setIsLoading(true); |
| | setError(null); |
| | try { |
| | const response = await listAPIKeys(); |
| | setApiKeys(response.api_keys || []); |
| | } catch (err) { |
| | setError(err.message || "Failed to load API keys"); |
| | } finally { |
| | setIsLoading(false); |
| | } |
| | }; |
| |
|
| | const handleCreateKey = async () => { |
| | if (!newKeyName.trim()) { |
| | setError("Please enter a name for your API key"); |
| | return; |
| | } |
| |
|
| | setIsCreating(true); |
| | setError(null); |
| | try { |
| | const response = await createAPIKey(newKeyName.trim()); |
| | setNewlyCreatedKey(response.api_key); |
| | setNewKeyName(""); |
| | setShowCreateModal(false); |
| | await fetchAPIKeys(); |
| | } catch (err) { |
| | setError(err.message || "Failed to create API key"); |
| | } finally { |
| | setIsCreating(false); |
| | } |
| | }; |
| |
|
| | const handleDeleteKey = async (keyId) => { |
| | if (!confirm("Are you sure you want to deactivate this API key? This action cannot be undone.")) { |
| | return; |
| | } |
| |
|
| | try { |
| | await deleteAPIKey(keyId); |
| | await fetchAPIKeys(); |
| | } catch (err) { |
| | setError(err.message || "Failed to delete API key"); |
| | } |
| | }; |
| |
|
| | const copyToClipboard = (text, keyId = null) => { |
| | navigator.clipboard.writeText(text); |
| | if (keyId) { |
| | setCopiedKeyId(keyId); |
| | setTimeout(() => setCopiedKeyId(null), 2000); |
| | } |
| | }; |
| |
|
| | const toggleKeyVisibility = (keyId) => { |
| | const newVisible = new Set(visibleKeys); |
| | if (newVisible.has(keyId)) { |
| | newVisible.delete(keyId); |
| | } else { |
| | newVisible.add(keyId); |
| | } |
| | setVisibleKeys(newVisible); |
| | }; |
| |
|
| | const formatDate = (dateString) => { |
| | if (!dateString) return "Never"; |
| | const date = new Date(dateString); |
| | return date.toLocaleDateString("en-US", { |
| | year: "numeric", |
| | month: "short", |
| | day: "numeric", |
| | hour: "2-digit", |
| | minute: "2-digit", |
| | }); |
| | }; |
| |
|
| | return ( |
| | <div className="min-h-screen bg-[#FAFAFA]"> |
| | {/* Header */} |
| | <header className="bg-white border-b border-slate-200/80 sticky top-0 z-40 h-16"> |
| | <div className="px-8 h-full flex items-center justify-between"> |
| | <div> |
| | <h1 className="text-xl font-bold text-slate-900 tracking-tight leading-tight"> |
| | API Keys |
| | </h1> |
| | <p className="text-sm text-slate-500 leading-tight"> |
| | Manage API keys for external application access |
| | </p> |
| | </div> |
| | <Button |
| | onClick={() => setShowCreateModal(true)} |
| | className="h-10 px-6 rounded-xl font-semibold bg-gradient-to-r from-indigo-600 to-violet-600 hover:from-indigo-700 hover:to-violet-700 shadow-lg shadow-indigo-500/25 hover:shadow-xl hover:shadow-indigo-500/30 transition-all duration-300" |
| | > |
| | <Plus className="h-4 w-4 mr-2" /> |
| | Create API Key |
| | </Button> |
| | </div> |
| | </header> |
| | |
| | {/* Main Content */} |
| | <div className="p-8"> |
| | {/* Error Message */} |
| | {error && ( |
| | <motion.div |
| | initial={{ opacity: 0, y: -10 }} |
| | animate={{ opacity: 1, y: 0 }} |
| | className="max-w-4xl mx-auto mb-6" |
| | > |
| | <div className="bg-red-50 border border-red-200 rounded-2xl p-4 flex items-start gap-3"> |
| | <AlertCircle className="h-5 w-5 text-red-600 flex-shrink-0 mt-0.5" /> |
| | <div className="flex-1"> |
| | <h3 className="font-semibold text-red-900 mb-1">Error</h3> |
| | <p className="text-sm text-red-700">{error}</p> |
| | </div> |
| | <button |
| | onClick={() => setError(null)} |
| | className="text-red-400 hover:text-red-600 transition-colors" |
| | > |
| | × |
| | </button> |
| | </div> |
| | </motion.div> |
| | )} |
| | |
| | {/* Success Message for New Key */} |
| | {newlyCreatedKey && ( |
| | <motion.div |
| | initial={{ opacity: 0, y: -10 }} |
| | animate={{ opacity: 1, y: 0 }} |
| | className="max-w-4xl mx-auto mb-6" |
| | > |
| | <div className="bg-emerald-50 border border-emerald-200 rounded-2xl p-6"> |
| | <div className="flex items-start gap-3 mb-4"> |
| | <div className="h-10 w-10 rounded-xl bg-emerald-100 flex items-center justify-center flex-shrink-0"> |
| | <Check className="h-5 w-5 text-emerald-600" /> |
| | </div> |
| | <div className="flex-1"> |
| | <h3 className="font-semibold text-emerald-900 mb-1"> |
| | API Key Created Successfully! |
| | </h3> |
| | <p className="text-sm text-emerald-700 mb-4"> |
| | ⚠️ Store this key securely - it will not be shown again. |
| | </p> |
| | <div className="bg-white rounded-xl p-4 border border-emerald-200"> |
| | <div className="flex items-center gap-2 mb-2"> |
| | <Key className="h-4 w-4 text-slate-500" /> |
| | <span className="text-xs font-medium text-slate-500 uppercase tracking-wide"> |
| | Your API Key |
| | </span> |
| | </div> |
| | <div className="flex items-center gap-2"> |
| | <code className="flex-1 font-mono text-sm text-slate-900 break-all"> |
| | {newlyCreatedKey} |
| | </code> |
| | <Button |
| | size="sm" |
| | variant="outline" |
| | onClick={() => copyToClipboard(newlyCreatedKey, "new")} |
| | className="flex-shrink-0" |
| | > |
| | {copiedKeyId === "new" ? ( |
| | <Check className="h-4 w-4 text-emerald-600" /> |
| | ) : ( |
| | <Copy className="h-4 w-4" /> |
| | )} |
| | </Button> |
| | </div> |
| | </div> |
| | </div> |
| | </div> |
| | <Button |
| | onClick={() => setNewlyCreatedKey(null)} |
| | className="w-full bg-emerald-600 hover:bg-emerald-700 text-white" |
| | > |
| | I've Saved My Key |
| | </Button> |
| | </div> |
| | </motion.div> |
| | )} |
| | |
| | {/* API Keys List */} |
| | <div className="max-w-4xl mx-auto"> |
| | {isLoading ? ( |
| | <div className="bg-white rounded-2xl border border-slate-200 p-12 text-center"> |
| | <div className="h-12 w-12 mx-auto rounded-2xl bg-indigo-100 flex items-center justify-center mb-4 animate-pulse"> |
| | <Key className="h-6 w-6 text-indigo-600" /> |
| | </div> |
| | <p className="text-slate-600">Loading API keys...</p> |
| | </div> |
| | ) : apiKeys.length === 0 ? ( |
| | <motion.div |
| | initial={{ opacity: 0, y: 20 }} |
| | animate={{ opacity: 1, y: 0 }} |
| | className="bg-white rounded-2xl border border-slate-200 p-12 text-center" |
| | > |
| | <div className="h-16 w-16 mx-auto rounded-2xl bg-slate-100 flex items-center justify-center mb-4"> |
| | <Key className="h-8 w-8 text-slate-400" /> |
| | </div> |
| | <h3 className="text-lg font-semibold text-slate-900 mb-2"> |
| | No API Keys Yet |
| | </h3> |
| | <p className="text-slate-500 mb-6"> |
| | Create your first API key to start using the API from external applications |
| | </p> |
| | <Button |
| | onClick={() => setShowCreateModal(true)} |
| | className="h-11 px-6 rounded-xl font-semibold bg-gradient-to-r from-indigo-600 to-violet-600 hover:from-indigo-700 hover:to-violet-700" |
| | > |
| | <Plus className="h-4 w-4 mr-2" /> |
| | Create Your First API Key |
| | </Button> |
| | </motion.div> |
| | ) : ( |
| | <div className="space-y-4"> |
| | {apiKeys.map((key) => ( |
| | <motion.div |
| | key={key.id} |
| | initial={{ opacity: 0, y: 20 }} |
| | animate={{ opacity: 1, y: 0 }} |
| | className="bg-white rounded-2xl border border-slate-200 p-6 hover:shadow-lg transition-shadow" |
| | > |
| | <div className="flex items-start justify-between mb-4"> |
| | <div className="flex-1"> |
| | <div className="flex items-center gap-3 mb-2"> |
| | <Key className="h-5 w-5 text-indigo-600" /> |
| | <h3 className="font-semibold text-slate-900">{key.name}</h3> |
| | {key.is_active ? ( |
| | <Badge className="bg-emerald-50 text-emerald-700 border-emerald-200"> |
| | Active |
| | </Badge> |
| | ) : ( |
| | <Badge className="bg-slate-100 text-slate-600 border-slate-200"> |
| | Inactive |
| | </Badge> |
| | )} |
| | </div> |
| | <div className="ml-8 space-y-2"> |
| | <div className="flex items-center gap-2"> |
| | <span className="font-mono text-sm text-slate-500 break-all"> |
| | {key.key_prefix} |
| | </span> |
| | </div> |
| | <div className="flex items-center gap-4 text-xs text-slate-400"> |
| | <div className="flex items-center gap-1"> |
| | <Clock className="h-3 w-3" /> |
| | <span>Created: {formatDate(key.created_at)}</span> |
| | </div> |
| | {key.last_used_at && ( |
| | <div className="flex items-center gap-1"> |
| | <Sparkles className="h-3 w-3" /> |
| | <span>Last used: {formatDate(key.last_used_at)}</span> |
| | </div> |
| | )} |
| | </div> |
| | </div> |
| | </div> |
| | <Button |
| | size="sm" |
| | variant="ghost" |
| | onClick={() => handleDeleteKey(key.id)} |
| | className="text-red-600 hover:text-red-700 hover:bg-red-50" |
| | disabled={!key.is_active} |
| | > |
| | <Trash2 className="h-4 w-4" /> |
| | </Button> |
| | </div> |
| | </motion.div> |
| | ))} |
| | </div> |
| | )} |
| | </div> |
| | |
| | {/* API Usage Guide */} |
| | <motion.div |
| | initial={{ opacity: 0, y: 20 }} |
| | animate={{ opacity: 1, y: 0 }} |
| | transition={{ delay: 0.2 }} |
| | className="max-w-4xl mx-auto mt-12" |
| | > |
| | <div className="bg-white rounded-2xl border border-slate-200 p-8"> |
| | <div className="flex items-center gap-3 mb-6"> |
| | <div className="h-12 w-12 rounded-xl bg-indigo-50 flex items-center justify-center"> |
| | <Code className="h-6 w-6 text-indigo-600" /> |
| | </div> |
| | <div> |
| | <h2 className="text-xl font-bold text-slate-900">How to Use API Keys</h2> |
| | <p className="text-sm text-slate-500"> |
| | Integrate document parsing into your external applications |
| | </p> |
| | </div> |
| | </div> |
| | |
| | <div className="space-y-6"> |
| | {/* Python Example */} |
| | <div className="bg-slate-50 rounded-xl p-6 border border-slate-200"> |
| | <div className="flex items-center gap-2 mb-4"> |
| | <FileText className="h-5 w-5 text-slate-600" /> |
| | <h3 className="font-semibold text-slate-900">Python Example</h3> |
| | </div> |
| | <pre className="bg-slate-900 text-slate-100 rounded-lg p-4 overflow-x-auto text-sm"> |
| | <code>{`import requests |
| | |
| | API_URL = "${API_BASE_URL}" |
| | API_KEY = "sk_live_YOUR_API_KEY_HERE" |
| | |
| | def extract_document(file_path, key_fields=None): |
| | with open(file_path, 'rb') as f: |
| | files = {'file': f} |
| | data = {} |
| | if key_fields: |
| | data['key_fields'] = key_fields |
| | |
| | response = requests.post( |
| | f"{API_URL}/api/extract", |
| | headers={"X-API-Key": API_KEY}, |
| | files=files, |
| | data=data |
| | ) |
| | return response.json() |
| | |
| | # Usage |
| | result = extract_document("invoice.pdf", |
| | key_fields="Invoice Number,Invoice Date") |
| | print(result)`}</code> |
| | </pre> |
| | <Button |
| | size="sm" |
| | variant="outline" |
| | onClick={() => copyToClipboard(`import requests |
| | |
| | API_URL = "${API_BASE_URL}" |
| | API_KEY = "sk_live_YOUR_API_KEY_HERE" |
| | |
| | def extract_document(file_path, key_fields=None): |
| | with open(file_path, 'rb') as f: |
| | files = {'file': f} |
| | data = {} |
| | if key_fields: |
| | data['key_fields'] = key_fields |
| | |
| | response = requests.post( |
| | f"{API_URL}/api/extract", |
| | headers={"X-API-Key": API_KEY}, |
| | files=files, |
| | data=data |
| | ) |
| | return response.json() |
| | |
| | # Usage |
| | result = extract_document("invoice.pdf", |
| | key_fields="Invoice Number,Invoice Date") |
| | print(result)`)} |
| | className="mt-3" |
| | > |
| | <Copy className="h-3 w-3 mr-2" /> |
| | Copy Code |
| | </Button> |
| | </div> |
| | |
| | {/* cURL Example */} |
| | <div className="bg-slate-50 rounded-xl p-6 border border-slate-200"> |
| | <div className="flex items-center gap-2 mb-4"> |
| | <FileText className="h-5 w-5 text-slate-600" /> |
| | <h3 className="font-semibold text-slate-900">cURL Example</h3> |
| | </div> |
| | <pre className="bg-slate-900 text-slate-100 rounded-lg p-4 overflow-x-auto text-sm"> |
| | <code>{`curl -X POST ${API_BASE_URL}/api/extract \\ |
| | -H "X-API-Key: sk_live_YOUR_API_KEY_HERE" \\ |
| | -F "file=@document.pdf" \\ |
| | -F "key_fields=Invoice Number,Invoice Date,Total Amount"`}</code> |
| | </pre> |
| | <Button |
| | size="sm" |
| | variant="outline" |
| | onClick={() => copyToClipboard(`curl -X POST ${API_BASE_URL}/api/extract \\ |
| | -H "X-API-Key: sk_live_YOUR_API_KEY_HERE" \\ |
| | -F "file=@document.pdf" \\ |
| | -F "key_fields=Invoice Number,Invoice Date,Total Amount"`)} |
| | className="mt-3" |
| | > |
| | <Copy className="h-3 w-3 mr-2" /> |
| | Copy Code |
| | </Button> |
| | </div> |
| | |
| | {/* JavaScript Example */} |
| | <div className="bg-slate-50 rounded-xl p-6 border border-slate-200"> |
| | <div className="flex items-center gap-2 mb-4"> |
| | <FileText className="h-5 w-5 text-slate-600" /> |
| | <h3 className="font-semibold text-slate-900">JavaScript/Node.js Example</h3> |
| | </div> |
| | <pre className="bg-slate-900 text-slate-100 rounded-lg p-4 overflow-x-auto text-sm"> |
| | <code>{`const FormData = require('form-data'); |
| | const fs = require('fs'); |
| | const axios = require('axios'); |
| | |
| | const API_URL = '${API_BASE_URL}'; |
| | const API_KEY = 'sk_live_YOUR_API_KEY_HERE'; |
| | |
| | async function extractDocument(filePath, keyFields = null) { |
| | const form = new FormData(); |
| | form.append('file', fs.createReadStream(filePath)); |
| | if (keyFields) { |
| | form.append('key_fields', keyFields); |
| | } |
| | |
| | const response = await axios.post( |
| | \`\${API_URL}/api/extract\`, |
| | form, |
| | { |
| | headers: { |
| | 'X-API-Key': API_KEY, |
| | ...form.getHeaders() |
| | } |
| | } |
| | ); |
| | return response.data; |
| | } |
| | |
| | // Usage |
| | extractDocument('invoice.pdf', 'Invoice Number,Invoice Date') |
| | .then(result => console.log(result));`}</code> |
| | </pre> |
| | <Button |
| | size="sm" |
| | variant="outline" |
| | onClick={() => copyToClipboard(`const FormData = require('form-data'); |
| | const fs = require('fs'); |
| | const axios = require('axios'); |
| | |
| | const API_URL = '${API_BASE_URL}'; |
| | const API_KEY = 'sk_live_YOUR_API_KEY_HERE'; |
| | |
| | async function extractDocument(filePath, keyFields = null) { |
| | const form = new FormData(); |
| | form.append('file', fs.createReadStream(filePath)); |
| | if (keyFields) { |
| | form.append('key_fields', keyFields); |
| | } |
| | |
| | const response = await axios.post( |
| | \`\${API_URL}/api/extract\`, |
| | form, |
| | { |
| | headers: { |
| | 'X-API-Key': API_KEY, |
| | ...form.getHeaders() |
| | } |
| | } |
| | ); |
| | return response.data; |
| | } |
| | |
| | // Usage |
| | extractDocument('invoice.pdf', 'Invoice Number,Invoice Date') |
| | .then(result => console.log(result));`)} |
| | className="mt-3" |
| | > |
| | <Copy className="h-3 w-3 mr-2" /> |
| | Copy Code |
| | </Button> |
| | </div> |
| | </div> |
| | |
| | <div className="mt-6 p-4 bg-indigo-50 rounded-xl border border-indigo-200"> |
| | <div className="flex items-start gap-3"> |
| | <AlertCircle className="h-5 w-5 text-indigo-600 flex-shrink-0 mt-0.5" /> |
| | <div className="flex-1"> |
| | <h4 className="font-semibold text-indigo-900 mb-1">API Endpoint</h4> |
| | <p className="text-sm text-indigo-700 mb-2"> |
| | <code className="bg-white px-2 py-1 rounded text-indigo-900"> |
| | POST {API_BASE_URL}/api/extract |
| | </code> |
| | </p> |
| | <p className="text-xs text-indigo-600"> |
| | • Max file size: 4 MB • Supported formats: PDF, PNG, JPEG, TIFF |
| | </p> |
| | </div> |
| | </div> |
| | </div> |
| | </div> |
| | </motion.div> |
| | </div> |
| | |
| | {/* Create API Key Modal */} |
| | <AnimatePresence> |
| | {showCreateModal && ( |
| | <motion.div |
| | initial={{ opacity: 0 }} |
| | animate={{ opacity: 1 }} |
| | exit={{ opacity: 0 }} |
| | className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4" |
| | onClick={() => !isCreating && setShowCreateModal(false)} |
| | > |
| | <motion.div |
| | initial={{ scale: 0.95, opacity: 0 }} |
| | animate={{ scale: 1, opacity: 1 }} |
| | exit={{ scale: 0.95, opacity: 0 }} |
| | onClick={(e) => e.stopPropagation()} |
| | className="bg-white rounded-2xl p-6 max-w-md w-full shadow-2xl" |
| | > |
| | <h2 className="text-xl font-bold text-slate-900 mb-2">Create New API Key</h2> |
| | <p className="text-sm text-slate-500 mb-6"> |
| | Give your API key a descriptive name to identify it later |
| | </p> |
| | <Input |
| | placeholder="e.g., Production API, Test Environment" |
| | value={newKeyName} |
| | onChange={(e) => setNewKeyName(e.target.value)} |
| | className="mb-6" |
| | onKeyPress={(e) => e.key === "Enter" && handleCreateKey()} |
| | /> |
| | <div className="flex gap-3"> |
| | <Button |
| | variant="outline" |
| | onClick={() => { |
| | setShowCreateModal(false); |
| | setNewKeyName(""); |
| | }} |
| | disabled={isCreating} |
| | className="flex-1" |
| | > |
| | Cancel |
| | </Button> |
| | <Button |
| | onClick={handleCreateKey} |
| | disabled={isCreating || !newKeyName.trim()} |
| | className="flex-1 bg-gradient-to-r from-indigo-600 to-violet-600 hover:from-indigo-700 hover:to-violet-700" |
| | > |
| | {isCreating ? "Creating..." : "Create Key"} |
| | </Button> |
| | </div> |
| | </motion.div> |
| | </motion.div> |
| | )} |
| | </AnimatePresence> |
| | </div> |
| | ); |
| | } |
| |
|
| |
|