gemini / src /components /accounts /AddAccountDialog.tsx
yinming
feat: Antigravity API Proxy for HuggingFace Spaces
bbb1195
import { useState } from 'react';
import { createPortal } from 'react-dom';
import { Plus, Loader2, CheckCircle2, XCircle, Key } from 'lucide-react';
import { useTranslation } from 'react-i18next';
interface AddAccountDialogProps {
onAdd: (email: string, refreshToken: string) => Promise<void>;
}
type Status = 'idle' | 'loading' | 'success' | 'error';
function AddAccountDialog({ onAdd }: AddAccountDialogProps) {
const { t } = useTranslation();
const [isOpen, setIsOpen] = useState(false);
const [refreshToken, setRefreshToken] = useState('');
// UI State
const [status, setStatus] = useState<Status>('idle');
const [message, setMessage] = useState('');
const resetState = () => {
setStatus('idle');
setMessage('');
setRefreshToken('');
};
const handleSubmit = async () => {
if (!refreshToken) {
setStatus('error');
setMessage(t('accounts.add.token.error_token'));
return;
}
setStatus('loading');
// 1. 尝试解析输入
let tokens: string[] = [];
const input = refreshToken.trim();
try {
// 尝试解析为 JSON
if (input.startsWith('[') && input.endsWith(']')) {
const parsed = JSON.parse(input);
if (Array.isArray(parsed)) {
tokens = parsed
.map((item: any) => item.refresh_token)
.filter((t: any) => typeof t === 'string' && t.startsWith('1//'));
}
}
} catch (e) {
// JSON 解析失败,忽略
console.debug('JSON parse failed, falling back to regex', e);
}
// 2. 如果 JSON 解析没有结果,尝试正则提取 (或者输入不是 JSON)
if (tokens.length === 0) {
const regex = /1\/\/[a-zA-Z0-9_\-]+/g;
const matches = input.match(regex);
if (matches) {
tokens = matches;
}
}
// 去重
tokens = [...new Set(tokens)];
if (tokens.length === 0) {
setStatus('error');
setMessage(t('accounts.add.token.error_token'));
return;
}
// 3. 批量添加
let successCount = 0;
let failCount = 0;
for (let i = 0; i < tokens.length; i++) {
const currentToken = tokens[i];
setMessage(t('accounts.add.token.batch_progress', { current: i + 1, total: tokens.length }));
try {
await onAdd("", currentToken);
successCount++;
} catch (error) {
console.error(`Failed to add token ${i + 1}:`, error);
failCount++;
}
// 稍微延迟一下,避免太快
await new Promise(r => setTimeout(r, 100));
}
// 4. 结果反馈
if (successCount === tokens.length) {
setStatus('success');
setMessage(t('accounts.add.token.batch_success', { count: successCount }));
setTimeout(() => {
setIsOpen(false);
resetState();
}, 1500);
} else if (successCount > 0) {
// 部分成功
setStatus('success');
setMessage(t('accounts.add.token.batch_partial', { success: successCount, fail: failCount }));
} else {
// 全部失败
setStatus('error');
setMessage(t('accounts.add.token.batch_fail'));
}
};
// 状态提示组件
const StatusAlert = () => {
if (status === 'idle' || !message) return null;
const styles = {
loading: 'alert-info',
success: 'alert-success',
error: 'alert-error'
};
const icons = {
loading: <Loader2 className="w-5 h-5 animate-spin" />,
success: <CheckCircle2 className="w-5 h-5" />,
error: <XCircle className="w-5 h-5" />
};
return (
<div className={`alert ${styles[status]} mb-4 text-sm py-2 shadow-sm`}>
{icons[status]}
<span>{message}</span>
</div>
);
};
return (
<>
<button
className="px-4 py-2 bg-white dark:bg-base-100 text-gray-700 dark:text-gray-300 text-sm font-medium rounded-lg hover:bg-gray-50 dark:hover:bg-base-200 transition-colors flex items-center gap-2 shadow-sm border border-gray-200/50 dark:border-base-300"
onClick={() => setIsOpen(true)}
>
<Plus className="w-4 h-4" />
{t('accounts.add_account')}
</button>
{isOpen && createPortal(
<dialog className="modal modal-open z-[100]">
<div className="modal-box bg-white dark:bg-base-100 text-gray-900 dark:text-base-content max-w-md">
<h3 className="font-bold text-lg mb-4">{t('accounts.add.title')}</h3>
{/* 状态提示区 */}
<StatusAlert />
<div className="space-y-4">
{/* Token 图标和说明 */}
<div className="text-center space-y-3">
<div className="bg-blue-50 dark:bg-blue-900/20 p-4 rounded-full w-16 h-16 mx-auto flex items-center justify-center">
<Key className="w-8 h-8 text-blue-500" />
</div>
<div className="space-y-1">
<h4 className="font-medium text-gray-900 dark:text-gray-100">
{t('accounts.add.token.label')}
</h4>
<p className="text-sm text-gray-500 dark:text-gray-400 max-w-xs mx-auto">
{t('accounts.add.token.cloud_hint') || 'Enter your Refresh Token to add an account'}
</p>
</div>
</div>
{/* Token 输入区 */}
<div className="bg-gray-50 dark:bg-base-200 p-4 rounded-lg border border-gray-200 dark:border-base-300">
<textarea
className="textarea textarea-bordered w-full h-32 font-mono text-xs leading-relaxed focus:outline-none focus:border-blue-500 transition-colors bg-white dark:bg-base-100 text-gray-900 dark:text-base-content border-gray-300 dark:border-base-300 placeholder:text-gray-400"
placeholder={t('accounts.add.token.placeholder')}
value={refreshToken}
onChange={(e) => setRefreshToken(e.target.value)}
disabled={status === 'loading' || status === 'success'}
/>
<p className="text-[10px] text-gray-400 mt-2">
{t('accounts.add.token.hint')}
</p>
</div>
</div>
<div className="flex gap-3 w-full mt-6">
<button
className="flex-1 px-4 py-2.5 bg-gray-100 dark:bg-base-200 text-gray-700 dark:text-gray-300 font-medium rounded-xl hover:bg-gray-200 dark:hover:bg-base-300 transition-colors focus:outline-none focus:ring-2 focus:ring-200 dark:focus:ring-base-300"
onClick={() => {
setIsOpen(false);
resetState();
}}
disabled={status === 'success'}
>
{t('accounts.add.btn_cancel')}
</button>
<button
className="flex-1 px-4 py-2.5 text-white font-medium rounded-xl shadow-md transition-all focus:outline-none focus:ring-2 focus:ring-offset-2 bg-blue-500 hover:bg-blue-600 focus:ring-blue-500 shadow-blue-100 dark:shadow-blue-900/30 flex justify-center items-center gap-2"
onClick={handleSubmit}
disabled={status === 'loading' || status === 'success'}
>
{status === 'loading' ? <Loader2 className="w-4 h-4 animate-spin" /> : null}
{t('accounts.add.btn_confirm')}
</button>
</div>
</div>
<div className="modal-backdrop bg-black/40 backdrop-blur-sm fixed inset-0 z-[-1]" onClick={() => setIsOpen(false)}></div>
</dialog>,
document.body
)}
</>
);
}
export default AddAccountDialog;