codex-ai-platform / src /pages /dashboard /DeveloperHub.tsx
3v324v23's picture
chore: 彻底清理项目,符合 Hugging Face 部署规范
ae4ceef
Raw
History Blame Contribute Delete
44.5 kB
import React, { useState, useEffect, useMemo } from 'react';
import {
Key, Plus, Trash2, Copy, Check, Shield,
Loader2, RefreshCw, KeyRound,
MoreVertical, Edit2, BookOpen,
Zap, CreditCard, Terminal,
Layers, Search, Filter, ArrowRight,
BarChart3, Cpu, Activity, Clock, X
} from 'lucide-react';
import { useStore } from '@/store/useStore';
import { motion, AnimatePresence } from 'framer-motion';
import { useTranslation } from 'react-i18next';
import PageContainer from '@/components/PageContainer';
interface ApiKey {
id: string;
key_name: string;
key_secret: string;
last_used: string | null;
created_at: string;
status: 'active' | 'inactive';
scopes: string[];
}
type Tab = 'overview' | 'keys' | 'docs';
type Lang = 'curl' | 'python' | 'node';
export default function DeveloperHub() {
const { t } = useTranslation();
const { token } = useStore();
const [activeTab, setActiveTab] = useState<Tab>('overview');
const [keys, setKeys] = useState<ApiKey[]>([]);
const [loading, setLoading] = useState(true);
const [isCreating, setIsCreating] = useState(false);
const [newKeyName, setNewKeyName] = useState('');
const [createdKey, setCreatedKey] = useState<{ key_name: string, key_secret: string } | null>(null);
const [copied, setCopied] = useState<string | null>(null);
const [showMenuId, setShowMenuId] = useState<string | null>(null);
const [editingId, setEditingId] = useState<string | null>(null);
const [editName, setEditName] = useState('');
// Search & Filter
const [searchQuery, setSearchQuery] = useState('');
const [statusFilter, setStatusFilter] = useState<'all' | 'active' | 'inactive'>('all');
// Docs 相关
const [selectedKeyForDocs, setSelectedKeyForDocs] = useState<string>('YOUR_API_KEY');
const [selectedLang, setSelectedLang] = useState<Lang>('curl');
const [isRevealing, setIsRevealing] = useState<string | null>(null);
const fetchKeys = async () => {
try {
const response = await fetch('/api/apikey/list', {
headers: { 'Authorization': `Bearer ${token}` }
});
const data = await response.json();
if (data.success) {
setKeys(data.keys);
}
} catch {
// Ignore
}
setLoading(false);
};
useEffect(() => {
fetchKeys();
}, [token]);
// 点击外部关闭菜单
useEffect(() => {
const handleClickOutside = () => setShowMenuId(null);
document.addEventListener('click', handleClickOutside);
return () => document.removeEventListener('click', handleClickOutside);
}, []);
const filteredKeys = useMemo(() => {
return keys.filter(k => {
const matchesSearch = k.key_name.toLowerCase().includes(searchQuery.toLowerCase());
const matchesStatus = statusFilter === 'all' || k.status === statusFilter;
return matchesSearch && matchesStatus;
});
}, [keys, searchQuery, statusFilter]);
const handleCreate = async (e: React.FormEvent) => {
e.preventDefault();
if (!newKeyName.trim()) return;
try {
const response = await fetch('/api/apikey/create', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ name: newKeyName.trim(), scopes: ['all'] }),
});
const data = await response.json();
if (data.success) {
setCreatedKey(data.key);
setIsCreating(false);
setNewKeyName('');
fetchKeys();
}
} catch {
// Ignore
}
};
const handleDelete = async (id: string) => {
if (!confirm(t('developer.keys.confirm_delete'))) return;
try {
await fetch(`/api/apikey/${id}`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${token}` }
});
fetchKeys();
} catch {
// Ignore
}
};
const handleToggleStatus = async (key: ApiKey) => {
const newStatus = key.status === 'active' ? 'inactive' : 'active';
try {
await fetch(`/api/apikey/${key.id}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ status: newStatus }),
});
fetchKeys();
} catch {
// Ignore
}
};
const handleUpdateName = async (id: string) => {
if (!editName.trim()) return;
try {
await fetch(`/api/apikey/${id}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ name: editName.trim() }),
});
setEditingId(null);
fetchKeys();
} catch {
// Ignore
}
};
const handleRegenerate = async (id: string) => {
if (!confirm(t('developer.keys.confirm_regenerate'))) return;
try {
const response = await fetch(`/api/apikey/${id}/regenerate`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}` }
});
const data = await response.json();
if (data.success) {
const keyName = keys.find(k => k.id === id)?.key_name || t('developer.keys.default_key_name');
setCreatedKey({ key_name: keyName, key_secret: data.key_secret });
fetchKeys();
}
} catch {
// Ignore
}
};
const handleRevealAndCopy = async (id: string) => {
setIsRevealing(id);
try {
const response = await fetch(`/api/apikey/${id}/reveal`, {
headers: { 'Authorization': `Bearer ${token}` }
});
const data = await response.json();
if (data.success) {
copyToClipboard(data.key_secret, id);
}
} catch {
// Ignore
}
setIsRevealing(null);
};
const copyToClipboard = (text: string, id: string = 'global') => {
navigator.clipboard.writeText(text);
setCopied(id);
setTimeout(() => setCopied(null), 2000);
};
const baseUrl = window.location.origin + '/api/v1';
// 获取显示的 Secret
const getSecretForDocs = () => {
if (selectedKeyForDocs === 'YOUR_API_KEY') return 'sk_********************';
const k = keys.find(k => k.id === selectedKeyForDocs);
return k ? k.key_secret : 'sk_********************';
};
// 生成代码片段
const getCodeSnippet = (lang: Lang) => {
const secret = getSecretForDocs();
const endpoint = `${baseUrl}/chat/completions`;
switch (lang) {
case 'curl':
return `curl ${endpoint} \\
-H "Content-Type: application/json" \\
-H "Authorization: Bearer ${secret}" \\
-d '{
"model": "codex-v1",
"messages": [{"role": "user", "content": "你好"}]
}'`;
case 'python':
return `import openai
client = openai.OpenAI(
api_key="${secret}",
base_url="${baseUrl}"
)
response = client.chat.completions.create(
model="codex-v1",
messages=[{"role": "user", "content": "你好"}]
)
print(response.choices[0].message.content)`;
case 'node':
return `import OpenAI from 'openai';
const client = new OpenAI({
apiKey: '${secret}',
baseURL: '${baseUrl}'
});
async function main() {
const response = await client.chat.completions.create({
model: 'codex-v1',
messages: [{ role: 'user', content: '你好' }],
});
console.log(response.choices[0].message.content);
}
main();`;
default:
return '';
}
};
return (
<PageContainer className="pb-24">
{/* Header & Tabs */}
<div className="mb-12">
<div className="flex flex-col md:flex-row md:items-end justify-between gap-8 mb-8">
<div className="flex items-center gap-5">
<div className="p-3.5 bg-gradient-to-br from-blue-600 to-indigo-700 rounded-[1.25rem] text-white shadow-xl shadow-blue-100 ring-4 ring-blue-50">
<Layers size={32} />
</div>
<div>
<h1 className="text-3xl font-extrabold text-zinc-900 tracking-tight">{t('developer.title')}</h1>
<p className="text-zinc-500 mt-2 text-lg font-medium">{t('developer.subtitle')}</p>
</div>
</div>
<div className="flex p-1.5 bg-white rounded-2xl w-fit border border-zinc-200 shadow-sm">
{(['overview', 'keys', 'docs'] as Tab[]).map((tab) => (
<button
key={tab}
onClick={() => setActiveTab(tab)}
className={`flex items-center gap-2.5 px-6 py-3 rounded-xl text-sm font-bold transition-all ${
activeTab === tab
? 'bg-zinc-900 text-white shadow-md'
: 'text-zinc-500 hover:text-zinc-900 hover:bg-zinc-50'
}`}
>
{tab === 'overview' && <Activity size={18} />}
{tab === 'keys' && <KeyRound size={18} />}
{tab === 'docs' && <BookOpen size={18} />}
{t(`developer.tabs.${tab}`)}
</button>
))}
</div>
</div>
</div>
<AnimatePresence mode="wait">
{activeTab === 'overview' && (
<motion.div
key="overview-tab"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
className="space-y-8"
>
{/* 快速开始卡片 */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="bg-gradient-to-br from-blue-600 to-blue-800 rounded-[2rem] p-8 text-white shadow-xl shadow-blue-200 relative overflow-hidden group">
<div className="absolute -right-4 -bottom-4 opacity-10 group-hover:scale-110 transition-transform duration-500">
<Cpu size={160} />
</div>
<h3 className="text-xl font-bold mb-2">{t('developer.quick_start.get_key')}</h3>
<p className="text-blue-100 text-sm leading-relaxed mb-6 opacity-80">
{t('developer.quick_start.get_key_desc')}
</p>
<button
onClick={() => setActiveTab('keys')}
className="flex items-center gap-2 bg-white text-blue-700 px-5 py-2.5 rounded-xl font-bold text-sm hover:shadow-lg transition-all"
>
{t('developer.quick_start.manage')} <ArrowRight size={16} />
</button>
</div>
<div className="bg-white border border-zinc-200 rounded-[2rem] p-8 shadow-sm hover:shadow-md transition-all group">
<div className="w-12 h-12 bg-orange-50 text-orange-600 rounded-2xl flex items-center justify-center mb-6 group-hover:scale-110 transition-transform">
<Zap size={24} />
</div>
<h3 className="text-xl font-bold text-zinc-900 mb-2">{t('developer.docs.title')}</h3>
<p className="text-zinc-500 text-sm leading-relaxed mb-6">
{t('developer.quick_start.interactive_docs_desc')}
</p>
<button
onClick={() => setActiveTab('docs')}
className="flex items-center gap-2 text-blue-600 font-bold text-sm hover:underline"
>
{t('developer.quick_start.read_docs')} <ArrowRight size={16} />
</button>
</div>
<div className="bg-white border border-zinc-200 rounded-[2rem] p-8 shadow-sm hover:shadow-md transition-all group">
<div className="w-12 h-12 bg-purple-50 text-purple-600 rounded-2xl flex items-center justify-center mb-6 group-hover:scale-110 transition-transform">
<BarChart3 size={24} />
</div>
<h3 className="text-xl font-bold text-zinc-900 mb-2">{t('developer.quick_start.usage_overview')}</h3>
<p className="text-zinc-500 text-sm leading-relaxed mb-6">
{t('developer.quick_start.usage_overview_desc')}
</p>
<button className="flex items-center gap-2 text-blue-600 font-bold text-sm hover:underline">
{t('developer.quick_start.view_stats')} <ArrowRight size={16} />
</button>
</div>
</div>
{/* 模拟统计图表部分 */}
<div className="bg-white border border-zinc-200 rounded-[2.5rem] p-10 shadow-sm">
<div className="flex items-center justify-between mb-8">
<div>
<h3 className="text-xl font-bold text-zinc-900">{t('developer.quick_start.call_trend')}</h3>
<p className="text-xs text-zinc-400 mt-1">{t('developer.quick_start.call_trend_desc')}</p>
</div>
<div className="flex gap-4">
<div className="flex items-center gap-2">
<div className="w-2.5 h-2.5 bg-blue-500 rounded-full"></div>
<span className="text-xs font-bold text-zinc-500">{t('developer.quick_start.requests')}</span>
</div>
</div>
</div>
<div className="h-48 flex items-end justify-between gap-4 px-4">
{[45, 78, 56, 92, 120, 88, 65].map((val, i) => (
<div key={i} className="flex-1 flex flex-col items-center gap-3">
<motion.div
initial={{ height: 0 }}
animate={{ height: `${val}%` }}
transition={{ delay: i * 0.1, duration: 1 }}
className="w-full bg-blue-500/10 hover:bg-blue-500/20 rounded-t-xl relative group transition-colors cursor-pointer"
>
<div className="absolute -top-8 left-1/2 -translate-x-1/2 bg-zinc-900 text-white text-[10px] px-2 py-1 rounded opacity-0 group-hover:opacity-100 transition-opacity">
{val * 10} req
</div>
</motion.div>
<span className="text-[10px] font-bold text-zinc-400">03-{13-6+i}</span>
</div>
))}
</div>
</div>
</motion.div>
)}
{activeTab === 'keys' && (
<motion.div
key="keys-tab"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
className="space-y-6"
>
{/* Keys Tab Header */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div className="flex items-center gap-4 flex-1 max-w-2xl">
<div className="relative flex-1">
<Search className="absolute left-4 top-1/2 -translate-y-1/2 text-zinc-400" size={18} />
<input
type="text"
placeholder={t('developer.keys.search_placeholder')}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-12 pr-4 py-3 bg-white border border-zinc-200 rounded-2xl focus:ring-2 focus:ring-blue-500 outline-none transition-all shadow-sm"
/>
</div>
<div className="relative">
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value as any)}
className="appearance-none pl-10 pr-10 py-3 bg-white border border-zinc-200 rounded-2xl focus:ring-2 focus:ring-blue-500 outline-none transition-all shadow-sm font-bold text-sm text-zinc-600"
>
<option value="all">{t('developer.keys.status_all')}</option>
<option value="active">{t('developer.keys.status_active')}</option>
<option value="inactive">{t('developer.keys.status_inactive')}</option>
</select>
<Filter className="absolute left-4 top-1/2 -translate-y-1/2 text-zinc-400" size={16} />
</div>
</div>
<button
onClick={() => setIsCreating(true)}
className="flex items-center gap-2 px-6 py-3 bg-blue-600 text-white rounded-2xl hover:bg-blue-700 transition-all shadow-lg shadow-blue-100 font-bold text-sm"
>
<Plus size={20} />
{t('developer.keys.new_key')}
</button>
</div>
{loading ? (
<div className="h-64 flex items-center justify-center">
<Loader2 size={32} className="animate-spin text-blue-600" />
</div>
) : (
<div className="bg-white border border-zinc-200 rounded-[2.5rem] shadow-sm overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full text-left">
<thead className="bg-zinc-50/50 border-b border-zinc-100">
<tr>
<th className="px-8 py-5 text-xs font-bold text-zinc-400 uppercase tracking-wider">{t('developer.keys.table.name')}</th>
<th className="px-8 py-5 text-xs font-bold text-zinc-400 uppercase tracking-wider">{t('developer.keys.table.key')}</th>
<th className="px-8 py-5 text-xs font-bold text-zinc-400 uppercase tracking-wider">{t('developer.keys.table.status')}</th>
<th className="px-8 py-5 text-xs font-bold text-zinc-400 uppercase tracking-wider">{t('developer.keys.table.last_used')}</th>
<th className="px-8 py-5 text-right pr-10">{t('developer.keys.table.actions')}</th>
</tr>
</thead>
<tbody className="divide-y divide-zinc-50">
{filteredKeys.map((key) => (
<tr key={key.id} className="hover:bg-zinc-50/30 transition-colors group">
<td className="px-8 py-5">
{editingId === key.id ? (
<div className="flex items-center gap-2">
<input
autoFocus
className="px-3 py-1.5 text-sm border border-zinc-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none w-40"
value={editName}
onChange={(e) => setEditName(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') handleUpdateName(key.id);
if (e.key === 'Escape') setEditingId(null);
}}
/>
<button onClick={() => handleUpdateName(key.id)} className="p-1.5 text-green-600 hover:bg-green-50 rounded-lg"><Check size={16} /></button>
<button onClick={() => setEditingId(null)} className="p-1.5 text-zinc-400 hover:bg-zinc-50 rounded-lg"><X size={16} /></button>
</div>
) : (
<div className="flex items-center gap-2">
<span className="text-sm font-bold text-zinc-900">{key.key_name}</span>
<button
onClick={() => { setEditingId(key.id); setEditName(key.key_name); }}
className="opacity-0 group-hover:opacity-100 p-1 text-zinc-400 hover:text-blue-600 transition-all"
>
<Edit2 size={12} />
</button>
</div>
)}
<div className="text-[10px] text-zinc-400 mt-1">{t('developer.keys.table.created_at', { date: new Date(key.created_at).toLocaleDateString() })}</div>
</td>
<td className="px-8 py-5">
<div className="flex items-center gap-2">
<code className="bg-zinc-100 px-3 py-1.5 rounded-xl text-xs font-mono text-zinc-500 border border-zinc-200/50 shadow-inner min-w-[120px]">
{key.key_secret}
</code>
<button
onClick={() => handleRevealAndCopy(key.id)}
className={`p-2 rounded-xl transition-all border ${
copied === key.id
? 'bg-green-50 text-green-600 border-green-200'
: 'bg-white text-zinc-400 border-zinc-200 hover:border-blue-400 hover:text-blue-600'
}`}
disabled={isRevealing === key.id}
title="复制完整密钥"
>
{isRevealing === key.id ? (
<Loader2 size={16} className="animate-spin" />
) : copied === key.id ? (
<Check size={16} />
) : (
<Copy size={16} />
)}
</button>
</div>
</td>
<td className="px-8 py-5">
<button
onClick={() => handleToggleStatus(key)}
className={`inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-[11px] font-bold border transition-all ${
key.status === 'active'
? 'bg-green-50 text-green-700 border-green-200 hover:bg-green-100'
: 'bg-zinc-100 text-zinc-500 border-zinc-200 hover:bg-zinc-200'
}`}
>
<span className={`w-1.5 h-1.5 rounded-full ${key.status === 'active' ? 'bg-green-500' : 'bg-zinc-400'}`}></span>
{key.status === 'active' ? t('developer.keys.status_active') : t('developer.keys.status_inactive')}
</button>
</td>
<td className="px-8 py-5 text-xs text-zinc-500 font-medium">
<div className="flex items-center gap-2">
<Clock size={12} className="text-zinc-300" />
{key.last_used ? new Date(key.last_used).toLocaleString() : t('developer.keys.table.never_used')}
</div>
</td>
<td className="px-8 py-5 text-right pr-10">
<div className="flex items-center justify-end gap-1">
<div className="relative">
<button
onClick={(e) => {
e.stopPropagation();
setShowMenuId(showMenuId === key.id ? null : key.id);
}}
className="p-2 text-zinc-400 hover:text-zinc-900 hover:bg-zinc-100 rounded-xl transition-all"
>
<MoreVertical size={18} />
</button>
{showMenuId === key.id && (
<div className="absolute right-0 top-full mt-2 w-44 bg-white border border-zinc-200 rounded-2xl shadow-xl z-20 py-1.5 overflow-hidden ring-1 ring-black/5">
<button
onClick={() => handleRegenerate(key.id)}
className="w-full text-left px-4 py-2.5 text-xs font-bold text-zinc-700 hover:bg-zinc-50 flex items-center gap-2.5"
>
<RefreshCw size={14} className="text-zinc-400" /> {t('developer.keys.menu.regenerate')}
</button>
<div className="h-px bg-zinc-100 my-1" />
<button
onClick={() => handleDelete(key.id)}
className="w-full text-left px-4 py-2.5 text-xs font-bold text-red-600 hover:bg-red-50 flex items-center gap-2.5"
>
<Trash2 size={14} className="text-red-400" /> {t('developer.keys.menu.delete')}
</button>
</div>
)}
</div>
</div>
</td>
</tr>
))}
{filteredKeys.length === 0 && (
<tr>
<td colSpan={5} className="px-8 py-20 text-center">
<div className="flex flex-col items-center gap-4 text-zinc-300">
<div className="p-4 bg-zinc-50 rounded-full">
<Key size={40} className="opacity-20" />
</div>
<div className="space-y-1">
<p className="text-sm font-bold text-zinc-500">{t('developer.keys.no_match')}</p>
<p className="text-xs text-zinc-400">{t('developer.keys.no_match_desc')}</p>
</div>
</div>
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
)}
</motion.div>
)}
{activeTab === 'docs' && (
<motion.div
key="docs-tab"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
className="space-y-10"
>
{/* Docs Tab Content */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6 bg-blue-50 border border-blue-100 rounded-[2.5rem] p-10 shadow-sm relative overflow-hidden">
<div className="absolute top-0 right-0 w-1/3 h-full bg-gradient-to-l from-blue-500/5 to-transparent pointer-events-none"></div>
<div className="space-y-3 z-10">
<h3 className="text-2xl font-black flex items-center gap-3 text-blue-900">
<Terminal className="text-blue-600" size={28} />
{t('developer.docs.title')}
</h3>
<p className="text-blue-700/70 text-sm max-w-md font-medium">
{t('developer.docs.subtitle')}
</p>
</div>
<div className="flex flex-col gap-2 z-10">
<label className="text-[10px] font-bold text-blue-400 uppercase tracking-[0.2em] ml-1">{t('developer.docs.select_key')}</label>
<div className="flex items-center gap-3">
<select
className="bg-white border border-blue-200 text-blue-900 text-sm font-bold rounded-2xl px-5 py-3 outline-none focus:ring-2 focus:ring-blue-500 min-w-[220px] transition-all shadow-sm"
value={selectedKeyForDocs}
onChange={(e) => setSelectedKeyForDocs(e.target.value)}
>
<option value="YOUR_API_KEY">{t('developer.docs.default_key')}</option>
{keys.filter(k => k.status === 'active').map(k => (
<option key={k.id} value={k.id}>{k.key_name}</option>
))}
</select>
{selectedKeyForDocs !== 'YOUR_API_KEY' && (
<button
onClick={async () => {
const k = keys.find(k => k.id === selectedKeyForDocs);
if (k) {
setIsRevealing('docs-reveal');
const res = await fetch(`/api/apikey/${k.id}/reveal`, {
headers: { 'Authorization': `Bearer ${token}` }
});
const data = await res.json();
if (data.success) {
copyToClipboard(data.key_secret, 'docs-reveal');
}
setIsRevealing(null);
}
}}
className="p-3 bg-white hover:bg-blue-50 rounded-2xl text-blue-600 hover:text-blue-700 transition-all border border-blue-200 shadow-sm"
title="复制完整 Key"
>
{isRevealing === 'docs-reveal' ? <Loader2 size={18} className="animate-spin" /> : <Copy size={18} />}
</button>
)}
</div>
</div>
</div>
<div className="grid grid-cols-1 gap-12">
{/* API 1: Chat */}
<section className="space-y-8">
<div className="flex items-center justify-between border-b border-zinc-100 pb-6">
<div className="flex items-center gap-4">
<div className="p-3 bg-orange-50 text-orange-600 rounded-[1.25rem]">
<Zap size={24} />
</div>
<div>
<h4 className="text-xl font-bold text-zinc-900">{t('developer.docs.chat_title')}</h4>
<p className="text-sm text-zinc-500 mt-1">{t('developer.docs.chat_desc')}</p>
</div>
</div>
<div className="flex items-center gap-3">
<span className="px-3 py-1 bg-green-50 text-green-700 rounded-xl text-xs font-black border border-green-200">POST</span>
<code className="text-xs font-bold text-zinc-400 bg-zinc-50 px-3 py-1 rounded-xl">/chat/completions</code>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-10">
<div className="space-y-6">
<div className="bg-white border border-zinc-200 rounded-[2rem] p-8 space-y-6 shadow-sm">
<div className="space-y-2">
<h5 className="text-[10px] font-bold text-zinc-400 uppercase tracking-[0.2em]">{t('developer.docs.auth_header')}</h5>
<div className="bg-zinc-900 rounded-2xl p-5 font-mono text-[11px] text-zinc-300 leading-relaxed shadow-inner">
Authorization: Bearer {selectedKeyForDocs === 'YOUR_API_KEY' ? 'sk_********************' : keys.find(k => k.id === selectedKeyForDocs)?.key_secret}
</div>
</div>
<div className="p-4 bg-blue-50/50 border border-blue-100 rounded-2xl flex gap-4">
<div className="w-8 h-8 bg-white rounded-lg flex items-center justify-center text-blue-600 shrink-0 shadow-sm">
<Shield size={18} />
</div>
<p className="text-[11px] text-blue-700/80 leading-relaxed">
{t('developer.docs.https_tip')}
</p>
</div>
<div className="space-y-4">
<h5 className="text-[10px] font-bold text-zinc-400 uppercase tracking-[0.2em]">{t('developer.docs.params')}</h5>
<div className="space-y-3">
{[
{ name: 'messages', type: 'Array', desc: t('developer.docs.param_messages'), req: true },
{ name: 'model', type: 'String', desc: t('developer.docs.param_model'), req: false },
{ name: 'stream', type: 'Boolean', desc: t('developer.docs.param_stream'), req: false },
].map((param) => (
<div key={param.name} className="flex justify-between items-center py-2 border-b border-zinc-50 last:border-0">
<div>
<span className="text-xs font-bold text-zinc-900">{param.name}</span>
<span className="text-[10px] text-zinc-400 ml-2">({param.type})</span>
</div>
<div className="flex items-center gap-3">
<span className="text-[10px] text-zinc-500">{param.desc}</span>
<span className={`text-[9px] font-bold px-1.5 py-0.5 rounded ${param.req ? 'bg-blue-50 text-blue-600' : 'bg-zinc-100 text-zinc-400'}`}>
{param.req ? t('developer.docs.required') : t('developer.docs.optional')}
</span>
</div>
</div>
))}
</div>
</div>
</div>
</div>
<div className="space-y-6">
<div className="bg-zinc-50 rounded-[2.5rem] overflow-hidden shadow-sm border border-zinc-200 flex flex-col h-full">
<div className="flex items-center justify-between px-8 py-4 bg-white border-b border-zinc-200">
<div className="flex p-1 bg-zinc-100 rounded-xl border border-zinc-200">
{(['curl', 'python', 'node'] as Lang[]).map((lang) => (
<button
key={lang}
onClick={() => setSelectedLang(lang)}
className={`px-4 py-1.5 rounded-lg text-[10px] font-black transition-all ${
selectedLang === lang
? 'bg-white text-blue-600 shadow-sm'
: 'text-zinc-500 hover:text-zinc-700'
}`}
>
{lang.toUpperCase()}
</button>
))}
</div>
<button
onClick={() => {
const code = getCodeSnippet(selectedLang);
const k = keys.find(k => k.id === selectedKeyForDocs);
if (k) {
handleRevealAndCopy(k.id);
} else {
copyToClipboard(code, `code-${selectedLang}`);
}
}}
className="flex items-center gap-2 text-zinc-400 hover:text-blue-600 transition-colors"
>
{copied === `code-${selectedLang}` || (selectedKeyForDocs !== 'YOUR_API_KEY' && copied === selectedKeyForDocs) ? (
<><Check size={14} className="text-green-600" /> <span className="text-[10px] font-bold text-green-600">{t('developer.keys.modal.copied')}</span></>
) : (
<><Copy size={14} /> <span className="text-[10px] font-bold">{t('developer.keys.modal.copy_code')}</span></>
)}
</button>
</div>
<div className="p-8 flex-1 overflow-auto custom-scrollbar">
<pre className="text-[11px] text-zinc-700 font-mono leading-relaxed">
{getCodeSnippet(selectedLang)}
</pre>
</div>
</div>
</div>
</div>
</section>
{/* API 2: Quota */}
<section className="space-y-8">
<div className="flex items-center justify-between border-b border-zinc-100 pb-6">
<div className="flex items-center gap-4">
<div className="p-3 bg-purple-50 text-purple-600 rounded-[1.25rem]">
<CreditCard size={24} />
</div>
<div>
<h4 className="text-xl font-bold text-zinc-900">{t('developer.docs.quota_title')}</h4>
<p className="text-sm text-zinc-500 mt-1">{t('developer.docs.quota_desc')}</p>
</div>
</div>
<div className="flex items-center gap-3">
<span className="px-3 py-1 bg-blue-50 text-blue-700 rounded-xl text-xs font-black border border-blue-200">GET</span>
<code className="text-xs font-bold text-zinc-400 bg-zinc-50 px-3 py-1 rounded-xl">/user/quota</code>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-10">
<div className="bg-zinc-50 rounded-[2.5rem] p-8 shadow-sm border border-zinc-200">
<div className="flex items-center justify-between mb-6">
<span className="text-[10px] font-bold text-zinc-400 uppercase tracking-widest">{t('developer.docs.req_example')}</span>
</div>
<pre className="text-[11px] text-zinc-700 font-mono leading-relaxed bg-white p-6 rounded-2xl border border-zinc-100">
{`curl ${baseUrl}/user/quota \\
-H "Authorization: Bearer ${selectedKeyForDocs === 'YOUR_API_KEY' ? 'YOUR_API_KEY' : 'FULL_SECRET'}"`}
</pre>
</div>
<div className="bg-white border border-zinc-200 rounded-[2.5rem] p-8 shadow-sm">
<div className="flex items-center justify-between mb-6">
<span className="text-[10px] font-bold text-zinc-400 uppercase tracking-widest">{t('developer.docs.res_example')}</span>
</div>
<pre className="text-[11px] text-zinc-600 font-mono leading-relaxed bg-zinc-50 p-6 rounded-2xl border border-zinc-100 shadow-inner">
{`{
"email": "user@example.com",
"plan": "pro",
"quota_remaining": 492
}`}
</pre>
</div>
</div>
</section>
</div>
</motion.div>
)}
</AnimatePresence>
{/* 创建模态框 (与之前一致,保持简洁) */}
<AnimatePresence>
{isCreating && (
<div className="fixed inset-0 z-[60] flex items-center justify-center bg-black/60 backdrop-blur-sm p-4">
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
className="bg-white rounded-[2.5rem] w-full max-w-md overflow-hidden shadow-2xl"
>
<div className="p-8 border-b border-zinc-100 flex items-center justify-between">
<h3 className="text-xl font-bold text-zinc-900">{t('developer.keys.modal.new_key_title')}</h3>
<button onClick={() => setIsCreating(false)} className="p-2 text-zinc-400 hover:text-zinc-600 hover:bg-zinc-100 rounded-xl transition-all">
<X size={20} />
</button>
</div>
<form onSubmit={handleCreate} className="p-8 space-y-6">
<div className="space-y-2">
<label className="text-[10px] font-bold text-zinc-400 uppercase tracking-widest ml-1">{t('developer.keys.modal.key_name_label')}</label>
<input
required
autoFocus
value={newKeyName}
onChange={e => setNewKeyName(e.target.value)}
placeholder={t('developer.keys.modal.key_name_placeholder')}
className="w-full px-5 py-4 bg-zinc-50 border border-zinc-200 rounded-2xl focus:ring-2 focus:ring-blue-500 outline-none transition-all"
/>
</div>
<div className="flex gap-4 pt-2">
<button
type="button"
onClick={() => setIsCreating(false)}
className="flex-1 py-4 bg-zinc-100 text-zinc-600 rounded-2xl font-bold hover:bg-zinc-200 transition-all"
>
{t('common.cancel')}
</button>
<button
type="submit"
className="flex-1 py-4 bg-blue-600 text-white rounded-2xl font-bold hover:bg-blue-700 transition-all shadow-lg shadow-blue-200"
>
{t('developer.keys.modal.create_button')}
</button>
</div>
</form>
</motion.div>
</div>
)}
{/* 成功展示模态框 (仅显示一次 Secret) */}
{createdKey && (
<div className="fixed inset-0 z-[70] flex items-center justify-center bg-black/60 backdrop-blur-sm p-4">
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
className="bg-white rounded-[2.5rem] w-full max-w-lg overflow-hidden shadow-2xl"
>
<div className="p-8 bg-green-50/50 border-b border-green-100">
<div className="flex items-center gap-4 text-green-700">
<div className="p-2 bg-white rounded-xl shadow-sm">
<Check size={24} />
</div>
<h3 className="text-xl font-bold">{t('developer.keys.modal.success_title')}</h3>
</div>
</div>
<div className="p-10 space-y-6 text-center">
<p className="text-sm text-zinc-600 leading-relaxed max-w-xs mx-auto">
{t('developer.keys.modal.success_desc')}
</p>
<div className="relative group">
<div className="w-full px-6 py-5 bg-zinc-900 text-zinc-100 rounded-[1.5rem] font-mono text-xs break-all pr-14 shadow-inner text-left leading-relaxed">
{createdKey.key_secret}
</div>
<button
onClick={() => copyToClipboard(createdKey.key_secret, 'new_key')}
className="absolute right-4 top-1/2 -translate-y-1/2 p-2.5 bg-white/10 hover:bg-white/20 rounded-xl text-zinc-100 transition-all border border-white/10"
>
{copied === 'new_key' ? <Check size={18} className="text-green-400" /> : <Copy size={18} />}
</button>
</div>
<button
onClick={() => setCreatedKey(null)}
className="w-full py-4 bg-zinc-900 text-white rounded-2xl font-bold hover:bg-zinc-800 transition-all shadow-xl shadow-zinc-200 mt-4"
>
{t('developer.keys.modal.saved_button')}
</button>
</div>
</motion.div>
</div>
)}
</AnimatePresence>
</PageContainer>
);
}
const CloseIcon = ({ size, className }: any) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2.5"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
>
<path d="M18 6 6 18"/><path d="m6 6 12 12"/>
</svg>
);
};