import { useState, useEffect, useMemo } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { Cloud, LogIn, LogOut, Loader2, Settings2, Printer as PrinterIcon, Droplet, X, Key, RefreshCw, Gauge, Pencil, Trash2, Save, AlertTriangle, Search, Plus, Copy, Clock, Layers, Filter, ChevronDown, ArrowUp, Upload, Download, Sparkles, Check, AlertCircle, Code, Sliders, List, Eye, EyeOff, GitCompare, ArrowRight, Equal, Minus as MinusIcon, Plus as PlusIcon, HardDrive, ShieldAlert, } from 'lucide-react'; import { api } from '../api/client'; import { formatRelativeTime } from '../utils/date'; import type { SlicerSetting, SlicerSettingsResponse, SlicerSettingDetail, SlicerSettingCreate, Printer, FieldDefinition, Permission } from '../api/client'; import { Card, CardContent } from '../components/Card'; import { Button } from '../components/Button'; import { useToast } from '../contexts/ToastContext'; import { useAuth } from '../contexts/AuthContext'; import { KProfilesView } from '../components/KProfilesView'; import { LocalProfilesView } from '../components/LocalProfilesView'; import { OrcaCloudView } from '../components/OrcaCloudView'; type TFunction = (key: string, options?: Record) => string; type ProfileTab = 'cloud' | 'orca_cloud' | 'local' | 'kprofiles'; type LoginStep = 'email' | 'code' | 'token'; type PresetType = 'all' | 'filament' | 'printer' | 'process'; // Extract metadata from preset name or inherits field function extractMetadata(name: string, inherits?: string): { printer: string | null; nozzle: string | null; layerHeight: string | null; filamentType: string | null; } { const searchIn = `${name} ${inherits || ''}`; // Extract printer (e.g., "X1C", "P1S", "A1", "H2D") const printerMatch = searchIn.match(/@?\s*(?:BBL\s+)?(?:Bambu\s+Lab\s+)?([XPAH][1-9][A-Z]?(?:\s*(?:Carbon|mini))?|H2D)/i); const printer = printerMatch ? printerMatch[1].trim() : null; // Extract nozzle size (e.g., "0.4 nozzle", "0.6mm") const nozzleMatch = searchIn.match(/(\d+\.?\d*)\s*(?:mm\s*)?nozzle|nozzle\s*(\d+\.?\d*)/i); const nozzle = nozzleMatch ? (nozzleMatch[1] || nozzleMatch[2]) + 'mm' : null; // Extract layer height (e.g., "0.20mm", "0.08mm Extra Fine") const layerMatch = searchIn.match(/(\d+\.?\d*)mm\s*(?:Standard|Fine|Extra Fine|Draft|Quality)?/i); const layerHeight = layerMatch ? layerMatch[1] + 'mm' : null; // Extract filament type (e.g., "PLA", "PETG", "ABS", "TPU") const filamentMatch = searchIn.match(/\b(PLA|PETG|ABS|ASA|TPU|PC|PA|PVA|HIPS|PP|PET(?:-?CF)?|PA(?:-?CF)?|PLA(?:-?CF)?)\b/i); const filamentType = filamentMatch ? filamentMatch[1].toUpperCase() : null; return { printer, nozzle, layerHeight, filamentType }; } // Check if preset is user-created (editable) function isUserPreset(settingId: string): boolean { return /^(P[FPM]US|PF\d|PP\d)/.test(settingId); } // ============================================================================ // LOGIN FORM // ============================================================================ export function LoginForm({ onSuccess, t }: { onSuccess: () => void; t: TFunction }) { const { showToast } = useToast(); const [step, setStep] = useState('email'); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [code, setCode] = useState(''); const [token, setToken] = useState(''); const [region, setRegion] = useState('global'); const [verificationType, setVerificationType] = useState<'email' | 'totp' | null>(null); const [tfaKey, setTfaKey] = useState(null); // Bambu is challenging this network with a CAPTCHA (#2790). A toast is the // wrong shape for it: nothing the user types will help, the remedy is to wait // or switch to a token, and both need to stay on screen while they read. const [captchaBlocked, setCaptchaBlocked] = useState(false); const loginMutation = useMutation({ mutationFn: () => api.cloudLogin(email, password, region), onSuccess: (result) => { setCaptchaBlocked(result.reason === 'captcha'); if (result.success) { showToast(t('profiles.login.toast.loggedIn')); onSuccess(); } else if (result.reason === 'captcha') { return; // The panel below says everything a toast could, and stays put. } else if (result.needs_verification) { setVerificationType(result.verification_type || 'email'); setTfaKey(result.tfa_key || null); if (result.verification_type === 'totp') { showToast(t('profiles.login.toast.enterTotp')); } else { showToast(t('profiles.login.toast.codeSent')); } setStep('code'); } else { showToast(result.message, 'error'); } }, onError: (error: Error) => { setCaptchaBlocked(false); showToast(error.message, 'error'); }, }); const verifyMutation = useMutation({ mutationFn: () => api.cloudVerify(email, code, tfaKey || undefined, region), onSuccess: (result) => { setCaptchaBlocked(result.reason === 'captcha'); if (result.success) { showToast(t('profiles.login.toast.loggedIn')); onSuccess(); } else if (result.reason !== 'captcha') { showToast(result.message, 'error'); } }, onError: (error: Error) => { setCaptchaBlocked(false); showToast(error.message, 'error'); }, }); const tokenMutation = useMutation({ mutationFn: () => api.cloudSetToken(token, region), onSuccess: () => { showToast(t('profiles.login.toast.tokenSet')); onSuccess(); }, onError: (error: Error) => showToast(error.message, 'error'), }); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (step === 'email') loginMutation.mutate(); else if (step === 'code') verifyMutation.mutate(); else if (step === 'token') tokenMutation.mutate(); }; const isPending = loginMutation.isPending || verifyMutation.isPending || tokenMutation.isPending; return (

{t('profiles.login.title')}

{t('profiles.login.subtitle')}

{captchaBlocked && step !== 'token' && (

{t('profiles.login.captchaTitle')}

{t('profiles.login.captchaBody')}

)}
{step === 'email' && ( <>
setEmail(e.target.value)} className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray-dark focus:border-bambu-green focus:outline-none" placeholder="your@email.com" required />
setPassword(e.target.value)} className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray-dark focus:border-bambu-green focus:outline-none" placeholder="••••••••" required />
)} {step === 'code' && (

{verificationType === 'totp' ? t('profiles.login.enterTotpHint') : t('profiles.login.checkEmail', { email })}

setCode(e.target.value.replace(/\D/g, '').slice(0, 6))} className="w-full px-3 py-3 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white text-center text-2xl tracking-widest font-mono focus:border-bambu-green focus:outline-none" placeholder="000000" maxLength={6} required />
)} {step === 'token' && ( <>

{t('profiles.login.accessTokenHint')}