vxkyyy commited on
Commit
e86cfc5
·
1 Parent(s): 5a282ed

feat: implement 2 free build limits and BYOK modal

Browse files
web/src/components/BillingModal.tsx ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from 'react';
2
+ import { api } from '../api';
3
+ import { motion, AnimatePresence } from 'framer-motion';
4
+
5
+ export const BillingModal = ({ isOpen, onClose, onKeySaved }: { isOpen: boolean, onClose: () => void, onKeySaved: () => void }) => {
6
+ const [apiKey, setApiKey] = useState('');
7
+ const [saving, setSaving] = useState(false);
8
+ const [error, setError] = useState('');
9
+
10
+ const handleSaveKey = async () => {
11
+ if (!apiKey.trim()) return;
12
+ setSaving(true);
13
+ setError('');
14
+ try {
15
+ await api.post('/profile/api-key', { api_key: apiKey.trim() });
16
+ onKeySaved();
17
+ onClose();
18
+ } catch (err: any) {
19
+ setError(err.response?.data?.detail || 'Failed to securely store API key.');
20
+ } finally {
21
+ setSaving(false);
22
+ }
23
+ };
24
+
25
+ if (!isOpen) return null;
26
+
27
+ return (
28
+ <AnimatePresence>
29
+ <div className="billing-modal-overlay">
30
+ <motion.div
31
+ className="sci-fi-card billing-modal-content"
32
+ initial={{ opacity: 0, scale: 0.95, y: 10 }}
33
+ animate={{ opacity: 1, scale: 1, y: 0 }}
34
+ exit={{ opacity: 0, scale: 0.95, y: 10 }}
35
+ transition={{ duration: 0.2 }}
36
+ >
37
+ <button className="billing-modal-close" onClick={onClose}>✕</button>
38
+
39
+ <div className="billing-header">
40
+ <div className="billing-icon">⚠️</div>
41
+ <h2 className="billing-title">Build Limit Reached</h2>
42
+ </div>
43
+
44
+ <p className="billing-sub">
45
+ You've used your 2 free autonomous chip builds! To continue generating RTL and running Silicon validations, you must provide your own LLM API key or upgrade your plan.
46
+ </p>
47
+
48
+ <div className="byok-section">
49
+ <h3 className="byok-title">Bring Your Own Key (BYOK)</h3>
50
+ <p className="byok-desc">
51
+ Enter an OpenAI, Anthropic, or Groq API key. Your key is <strong>AES-256 encrypted at rest</strong> using a secure server-side key and is never logged or exposed.
52
+ </p>
53
+
54
+ <input
55
+ className="byok-input"
56
+ type="password"
57
+ placeholder="sk-..."
58
+ value={apiKey}
59
+ onChange={e => setApiKey(e.target.value)}
60
+ autoFocus
61
+ />
62
+
63
+ {error && <div className="byok-error">{error}</div>}
64
+
65
+ <button
66
+ className="action-btn byok-submit"
67
+ onClick={handleSaveKey}
68
+ disabled={saving || !apiKey.trim()}
69
+ >
70
+ {saving ? (
71
+ <span>Encrypting & Saving...</span>
72
+ ) : (
73
+ <span>Save Encrypted Key →</span>
74
+ )}
75
+ </button>
76
+ </div>
77
+
78
+ <div className="billing-footer">
79
+ or <a href="mailto:sales@agentic.ai" className="billing-link">contact sales</a> for an Enterprise/Pro Plan.
80
+ </div>
81
+ </motion.div>
82
+ </div>
83
+ </AnimatePresence>
84
+ );
85
+ };
web/src/index.css CHANGED
@@ -3482,3 +3482,184 @@ body {
3482
  color: var(--fail);
3483
  border-color: var(--fail-bdr);
3484
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3482
  color: var(--fail);
3483
  border-color: var(--fail-bdr);
3484
  }
3485
+
3486
+ /* ==========================================================================
3487
+ BILLING MODAL (BYOK)
3488
+ ========================================================================== */
3489
+
3490
+ .billing-modal-overlay {
3491
+ position: fixed;
3492
+ top: 0;
3493
+ left: 0;
3494
+ width: 100vw;
3495
+ height: 100vh;
3496
+ background: rgba(12, 11, 10, 0.85);
3497
+ backdrop-filter: blur(8px);
3498
+ -webkit-backdrop-filter: blur(8px);
3499
+ z-index: 9999;
3500
+ display: flex;
3501
+ align-items: center;
3502
+ justify-content: center;
3503
+ padding: 1rem;
3504
+ }
3505
+
3506
+ .billing-modal-content {
3507
+ width: 100%;
3508
+ max-width: 480px;
3509
+ position: relative;
3510
+ padding: 2rem !important;
3511
+ background: linear-gradient(180deg, var(--bg-card) 0%, var(--bg) 100%) !important;
3512
+ box-shadow: 0 24px 64px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(255, 255, 255, 0.05) !important;
3513
+ overflow: visible !important;
3514
+ }
3515
+
3516
+ .billing-modal-close {
3517
+ position: absolute;
3518
+ top: 1.25rem;
3519
+ right: 1.25rem;
3520
+ background: transparent;
3521
+ border: none;
3522
+ color: var(--text-dim);
3523
+ font-size: 1.2rem;
3524
+ cursor: pointer;
3525
+ transition: color var(--fast);
3526
+ padding: 0.2rem;
3527
+ }
3528
+
3529
+ .billing-modal-close:hover {
3530
+ color: var(--text);
3531
+ }
3532
+
3533
+ .billing-header {
3534
+ display: flex;
3535
+ align-items: center;
3536
+ gap: 0.75rem;
3537
+ margin-bottom: 1rem;
3538
+ }
3539
+
3540
+ .billing-icon {
3541
+ font-size: 1.5rem;
3542
+ }
3543
+
3544
+ .billing-title {
3545
+ font-size: 1.25rem;
3546
+ font-weight: 700;
3547
+ color: var(--text);
3548
+ margin: 0;
3549
+ letter-spacing: -0.02em;
3550
+ }
3551
+
3552
+ .billing-sub {
3553
+ color: var(--text-mid);
3554
+ font-size: 0.95rem;
3555
+ line-height: 1.6;
3556
+ margin-bottom: 2rem;
3557
+ }
3558
+
3559
+ .byok-section {
3560
+ background: var(--bg);
3561
+ border: 1px solid var(--border-mid);
3562
+ border-radius: var(--radius-md);
3563
+ padding: 1.5rem;
3564
+ margin-bottom: 1.5rem;
3565
+ box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.1);
3566
+ }
3567
+
3568
+ .byok-title {
3569
+ font-size: 0.95rem;
3570
+ font-weight: 600;
3571
+ color: var(--text);
3572
+ margin: 0 0 0.5rem 0;
3573
+ display: flex;
3574
+ align-items: center;
3575
+ gap: 0.5rem;
3576
+ }
3577
+
3578
+ .byok-title::before {
3579
+ content: "🔑";
3580
+ font-size: 1rem;
3581
+ }
3582
+
3583
+ .byok-desc {
3584
+ font-size: 0.82rem;
3585
+ color: var(--text-dim);
3586
+ margin: 0 0 1.25rem 0;
3587
+ line-height: 1.5;
3588
+ }
3589
+
3590
+ .byok-desc strong {
3591
+ color: var(--text-mid);
3592
+ font-weight: 600;
3593
+ }
3594
+
3595
+ .byok-input {
3596
+ width: 100%;
3597
+ background: var(--bg-card);
3598
+ border: 1px solid var(--border);
3599
+ border-radius: var(--radius);
3600
+ padding: 0.85rem 1rem;
3601
+ color: var(--text);
3602
+ font-family: 'Fira Code', monospace;
3603
+ font-size: 0.9rem;
3604
+ outline: none;
3605
+ transition: all var(--fast);
3606
+ margin-bottom: 1rem;
3607
+ }
3608
+
3609
+ .byok-input:focus {
3610
+ border-color: var(--accent);
3611
+ box-shadow: 0 0 0 2px rgba(201, 100, 62, 0.15);
3612
+ }
3613
+
3614
+ .byok-error {
3615
+ color: var(--fail);
3616
+ font-size: 0.85rem;
3617
+ margin-bottom: 1rem;
3618
+ display: flex;
3619
+ align-items: center;
3620
+ gap: 0.4rem;
3621
+ }
3622
+
3623
+ .byok-error::before {
3624
+ content: "✕";
3625
+ font-weight: 700;
3626
+ }
3627
+
3628
+ .byok-submit {
3629
+ width: 100%;
3630
+ background: var(--text);
3631
+ color: var(--bg);
3632
+ border: none;
3633
+ padding: 0.85rem;
3634
+ display: flex;
3635
+ justify-content: center;
3636
+ align-items: center;
3637
+ }
3638
+
3639
+ .byok-submit:not(:disabled):hover {
3640
+ background: #FFF;
3641
+ transform: translateY(-1px);
3642
+ box-shadow: 0 4px 12px rgba(255, 255, 255, 0.15);
3643
+ }
3644
+
3645
+ .byok-submit:disabled {
3646
+ opacity: 0.5;
3647
+ cursor: not-allowed;
3648
+ }
3649
+
3650
+ .billing-footer {
3651
+ text-align: center;
3652
+ font-size: 0.85rem;
3653
+ color: var(--text-dim);
3654
+ }
3655
+
3656
+ .billing-link {
3657
+ color: var(--accent);
3658
+ text-decoration: none;
3659
+ font-weight: 500;
3660
+ transition: color var(--fast);
3661
+ }
3662
+
3663
+ .billing-link:hover {
3664
+ text-decoration: underline;
3665
+ }
web/src/pages/DesignStudio.tsx CHANGED
@@ -2,6 +2,7 @@ import { useState, useEffect, useRef } from 'react';
2
  import { motion, AnimatePresence } from 'framer-motion';
3
  import { BuildMonitor } from '../components/BuildMonitor';
4
  import { ChipSummary } from '../components/ChipSummary';
 
5
  import { fetchEventSource } from '@microsoft/fetch-event-source';
6
  import { api, API_BASE } from '../api';
7
 
@@ -44,6 +45,10 @@ export const DesignStudio = () => {
44
  const [result, setResult] = useState<any>(null);
45
  const [error, setError] = useState('');
46
 
 
 
 
 
47
  // Build Options
48
  const [skipOpenlane, setSkipOpenlane] = useState(false);
49
  const [skipCoverage, setSkipCoverage] = useState(false);
@@ -74,9 +79,26 @@ export const DesignStudio = () => {
74
  }
75
  }, [prompt]);
76
 
 
 
 
 
 
 
 
77
  const handleLaunch = async () => {
78
  if (!prompt.trim()) return;
79
  setError('');
 
 
 
 
 
 
 
 
 
 
80
  try {
81
  const res = await api.post(`/build`, {
82
  design_name: designName || slugify(prompt),
@@ -438,6 +460,15 @@ export const DesignStudio = () => {
438
  )}
439
 
440
  </AnimatePresence>
 
 
 
 
 
 
 
 
 
441
  </div>
442
  );
443
  };
 
2
  import { motion, AnimatePresence } from 'framer-motion';
3
  import { BuildMonitor } from '../components/BuildMonitor';
4
  import { ChipSummary } from '../components/ChipSummary';
5
+ import { BillingModal } from '../components/BillingModal';
6
  import { fetchEventSource } from '@microsoft/fetch-event-source';
7
  import { api, API_BASE } from '../api';
8
 
 
45
  const [result, setResult] = useState<any>(null);
46
  const [error, setError] = useState('');
47
 
48
+ // Billing / Profile State
49
+ const [profile, setProfile] = useState<{ auth_enabled: boolean, plan: string, successful_builds: number, has_byok_key: boolean } | null>(null);
50
+ const [showBillingModal, setShowBillingModal] = useState(false);
51
+
52
  // Build Options
53
  const [skipOpenlane, setSkipOpenlane] = useState(false);
54
  const [skipCoverage, setSkipCoverage] = useState(false);
 
79
  }
80
  }, [prompt]);
81
 
82
+ // Fetch Profile Limits
83
+ useEffect(() => {
84
+ api.get('/profile')
85
+ .then(res => setProfile(res.data))
86
+ .catch(() => setProfile(null)); // Ignored explicitly if no auth
87
+ }, []);
88
+
89
  const handleLaunch = async () => {
90
  if (!prompt.trim()) return;
91
  setError('');
92
+
93
+ // Billing Guard: enforce 2 free successful builds
94
+ if (profile?.auth_enabled) {
95
+ const { plan, successful_builds, has_byok_key } = profile;
96
+ if (plan === 'free' && successful_builds >= 2 && !has_byok_key) {
97
+ setShowBillingModal(true);
98
+ return;
99
+ }
100
+ }
101
+
102
  try {
103
  const res = await api.post(`/build`, {
104
  design_name: designName || slugify(prompt),
 
460
  )}
461
 
462
  </AnimatePresence>
463
+
464
+ <BillingModal
465
+ isOpen={showBillingModal}
466
+ onClose={() => setShowBillingModal(false)}
467
+ onKeySaved={() => {
468
+ // Update profile locally to unblock
469
+ setProfile(prev => prev ? { ...prev, has_byok_key: true } : null);
470
+ }}
471
+ />
472
  </div>
473
  );
474
  };