Spaces:
Sleeping
Sleeping
| 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()); | |
| // Fetch API keys on mount | |
| 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)} | |
| 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 text-sm text-slate-500"> | |
| <span className="font-mono">{key.key_prefix}</span> | |
| <Button | |
| size="sm" | |
| variant="ghost" | |
| onClick={() => toggleKeyVisibility(key.id)} | |
| className="h-6 px-2" | |
| > | |
| {visibleKeys.has(key.id) ? ( | |
| <EyeOff className="h-3 w-3" /> | |
| ) : ( | |
| <Eye className="h-3 w-3" /> | |
| )} | |
| </Button> | |
| </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> | |
| ); | |
| } | |