Karan6124 commited on
Commit
dfe618f
·
1 Parent(s): 850051a

feat: implement subscription packages, message limit quotas, and 3-day discount timer

Browse files

- Database: Add subscription_tier, messages_remaining, monthly_messages_used, and last_billing_date to User model
- Migration: Generate and apply Alembic migration with server_defaults for existing rows
- Backend: Enforce bearer token authorization and message limits on /analyst/chat endpoint
- Backend: Map Razorpay webhook payment capture to user subscription tiers and credit allocations
- Frontend: Implement live ticking 3-day countdown timer for Pro pack discount (₹10k vs ₹15k)
- Frontend: Show real-time remaining quota badge in Chatbot header and lock chat input when limit is reached

alembic/versions/bbdbf37359be_add_subscription_and_message_limit_.py ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """add_subscription_and_message_limit_columns
2
+
3
+ Revision ID: bbdbf37359be
4
+ Revises: 15639881888f
5
+ Create Date: 2026-07-04 19:07:25.250443
6
+
7
+ """
8
+ from typing import Sequence, Union
9
+
10
+ from alembic import op
11
+ import sqlalchemy as sa
12
+
13
+
14
+ # revision identifiers, used by Alembic.
15
+ revision: str = 'bbdbf37359be'
16
+ down_revision: Union[str, Sequence[str], None] = '15639881888f'
17
+ branch_labels: Union[str, Sequence[str], None] = None
18
+ depends_on: Union[str, Sequence[str], None] = None
19
+
20
+
21
+ def upgrade() -> None:
22
+ """Upgrade schema."""
23
+ # ### commands auto generated by Alembic - please adjust! ###
24
+ op.add_column('users', sa.Column('subscription_tier', sa.String(length=50), server_default='free', nullable=False))
25
+ op.add_column('users', sa.Column('messages_remaining', sa.Integer(), server_default='3', nullable=False))
26
+ op.add_column('users', sa.Column('monthly_messages_used', sa.Integer(), server_default='0', nullable=False))
27
+ op.add_column('users', sa.Column('last_billing_date', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False))
28
+ # ### end Alembic commands ###
29
+
30
+
31
+ def downgrade() -> None:
32
+ """Downgrade schema."""
33
+ # ### commands auto generated by Alembic - please adjust! ###
34
+ op.drop_column('users', 'last_billing_date')
35
+ op.drop_column('users', 'monthly_messages_used')
36
+ op.drop_column('users', 'messages_remaining')
37
+ op.drop_column('users', 'subscription_tier')
38
+ # ### end Alembic commands ###
backend/app/api/endpoints.py CHANGED
@@ -521,11 +521,29 @@ class ChatRequest(BaseModel):
521
  activeIndicators: Dict[str, bool]
522
 
523
  @router.post("/analyst/chat")
524
- async def chat_with_analyst(payload: ChatRequest):
 
 
 
 
525
  """
526
  Takes the user's message, drawing markers, active indicators, and chat history,
527
  and returns a cooperative analysis message from Gemini.
 
528
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
529
  ticker = payload.ticker
530
  message = payload.message
531
  history = payload.history
@@ -595,4 +613,21 @@ async def chat_with_analyst(payload: ChatRequest):
595
 
596
  from backend.app.services.gemini import generate_text
597
  response_text = await generate_text(full_prompt)
598
- return {"response": response_text}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
521
  activeIndicators: Dict[str, bool]
522
 
523
  @router.post("/analyst/chat")
524
+ async def chat_with_analyst(
525
+ payload: ChatRequest,
526
+ current_user: models.User = Depends(get_current_user),
527
+ db: AsyncSession = Depends(get_db)
528
+ ):
529
  """
530
  Takes the user's message, drawing markers, active indicators, and chat history,
531
  and returns a cooperative analysis message from Gemini.
532
+ Enforces subscription message limits.
533
  """
534
+ # 1. Refresh user billing cycles / credits
535
+ user = await crud.refresh_user_credits(db, current_user)
536
+ is_admin = (user.email == "karanshelar8775@gmail.com")
537
+
538
+ # 2. Check limits
539
+ if not is_admin:
540
+ if user.subscription_tier in ("free", "analyst", "trader"):
541
+ if user.messages_remaining <= 0:
542
+ raise HTTPException(status_code=403, detail="Quota exhausted. Please upgrade your plan.")
543
+ elif user.subscription_tier == "pro":
544
+ if user.monthly_messages_used >= 100:
545
+ raise HTTPException(status_code=403, detail="Monthly message quota of 100 exhausted.")
546
+
547
  ticker = payload.ticker
548
  message = payload.message
549
  history = payload.history
 
613
 
614
  from backend.app.services.gemini import generate_text
615
  response_text = await generate_text(full_prompt)
616
+
617
+ # 3. Deduct/Increment message counts
618
+ if not is_admin:
619
+ if user.subscription_tier in ("free", "analyst", "trader"):
620
+ user.messages_remaining = max(0, user.messages_remaining - 1)
621
+ elif user.subscription_tier == "pro":
622
+ user.monthly_messages_used += 1
623
+
624
+ db.add(user)
625
+ await db.commit()
626
+ await db.refresh(user)
627
+
628
+ return {
629
+ "response": response_text,
630
+ "subscription_tier": user.subscription_tier,
631
+ "messages_remaining": user.messages_remaining,
632
+ "monthly_messages_used": user.monthly_messages_used
633
+ }
backend/app/database/crud.py CHANGED
@@ -39,15 +39,28 @@ async def create_user(db: AsyncSession, user_in: schemas.UserBase, google_id: st
39
 
40
  async def refresh_user_credits(db: AsyncSession, user: models.User) -> models.User:
41
  """
42
- Checks if 7 days have passed since the last credit refresh.
43
- If so, resets credits back to 5.
 
44
  """
45
  now = datetime.datetime.now(datetime.timezone.utc)
46
- time_elapsed = now - user.last_credit_refresh
47
 
48
- if time_elapsed >= datetime.timedelta(days=7):
49
- user.credits = 5
50
- user.last_credit_refresh = now
 
 
 
 
 
 
 
 
 
 
 
 
51
  db.add(user)
52
  await db.commit()
53
  await db.refresh(user)
@@ -287,10 +300,24 @@ async def capture_payment_transaction(
287
  tx.status = "captured"
288
  db.add(tx)
289
 
290
- # 3. Credit the user
291
  user = await get_user(db, tx.user_id)
292
  if user:
293
  user.credits += tx.credits_credited
 
 
 
 
 
 
 
 
 
 
 
 
 
 
294
  db.add(user)
295
 
296
  await db.commit()
 
39
 
40
  async def refresh_user_credits(db: AsyncSession, user: models.User) -> models.User:
41
  """
42
+ Checks and applies user refreshes:
43
+ - For Free tier: resets credits to 5 every 7 days.
44
+ - For Pro tier: resets monthly_messages_used to 0 every 30 days.
45
  """
46
  now = datetime.datetime.now(datetime.timezone.utc)
47
+ updated = False
48
 
49
+ if user.subscription_tier == "free":
50
+ time_elapsed = now - user.last_credit_refresh
51
+ if time_elapsed >= datetime.timedelta(days=7):
52
+ user.credits = 5
53
+ user.last_credit_refresh = now
54
+ updated = True
55
+
56
+ if user.subscription_tier == "pro":
57
+ billing_elapsed = now - user.last_billing_date
58
+ if billing_elapsed >= datetime.timedelta(days=30):
59
+ user.monthly_messages_used = 0
60
+ user.last_billing_date = now
61
+ updated = True
62
+
63
+ if updated:
64
  db.add(user)
65
  await db.commit()
66
  await db.refresh(user)
 
300
  tx.status = "captured"
301
  db.add(tx)
302
 
303
+ # 3. Credit the user and update subscription tier
304
  user = await get_user(db, tx.user_id)
305
  if user:
306
  user.credits += tx.credits_credited
307
+
308
+ # Determine plan from transaction amount in Rupees
309
+ amt_rupees = tx.amount // 100
310
+ if amt_rupees == 500:
311
+ user.subscription_tier = "analyst"
312
+ user.messages_remaining += 10
313
+ elif amt_rupees == 1500:
314
+ user.subscription_tier = "trader"
315
+ user.messages_remaining += 25
316
+ elif amt_rupees in (10000, 15000):
317
+ user.subscription_tier = "pro"
318
+ user.monthly_messages_used = 0
319
+ user.last_billing_date = datetime.datetime.now(datetime.timezone.utc)
320
+
321
  db.add(user)
322
 
323
  await db.commit()
backend/app/database/models.py CHANGED
@@ -27,6 +27,16 @@ class User(Base):
27
  nullable=False
28
  )
29
 
 
 
 
 
 
 
 
 
 
 
30
  # Relationships
31
  watchlists: Mapped[List["Watchlist"]] = relationship(back_populates="user", cascade="all, delete-orphan")
32
  alerts: Mapped[List["Alert"]] = relationship(back_populates="user", cascade="all, delete-orphan")
 
27
  nullable=False
28
  )
29
 
30
+ # Subscription parameters
31
+ subscription_tier: Mapped[str] = mapped_column(String(50), default="free", nullable=False)
32
+ messages_remaining: Mapped[int] = mapped_column(Integer, default=3, nullable=False)
33
+ monthly_messages_used: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
34
+ last_billing_date: Mapped[datetime.datetime] = mapped_column(
35
+ DateTime(timezone=True),
36
+ server_default=func.now(),
37
+ nullable=False
38
+ )
39
+
40
  # Relationships
41
  watchlists: Mapped[List["Watchlist"]] = relationship(back_populates="user", cascade="all, delete-orphan")
42
  alerts: Mapped[List["Alert"]] = relationship(back_populates="user", cascade="all, delete-orphan")
backend/app/graphql/schema.py CHANGED
@@ -29,6 +29,10 @@ class UserType:
29
  credits: int
30
  last_credit_refresh: datetime.datetime
31
  created_at: datetime.datetime
 
 
 
 
32
 
33
  @strawberry.type
34
  class WatchlistType:
@@ -438,10 +442,8 @@ class Mutation:
438
  credits_credited = 10
439
  elif amount == 1500:
440
  credits_credited = 50
441
- elif amount == 2500:
442
  credits_credited = 100
443
- elif amount == 10500:
444
- credits_credited = 999999 # Code for Unlimited / Lifetime
445
  else:
446
  credits_credited = amount // 50
447
 
 
29
  credits: int
30
  last_credit_refresh: datetime.datetime
31
  created_at: datetime.datetime
32
+ subscription_tier: str
33
+ messages_remaining: int
34
+ monthly_messages_used: int
35
+ last_billing_date: datetime.datetime
36
 
37
  @strawberry.type
38
  class WatchlistType:
 
442
  credits_credited = 10
443
  elif amount == 1500:
444
  credits_credited = 50
445
+ elif amount in (10000, 15000):
446
  credits_credited = 100
 
 
447
  else:
448
  credits_credited = amount // 50
449
 
backend/app/schemas/schemas.py CHANGED
@@ -25,6 +25,10 @@ class UserResponse(UserBase):
25
  credits: int
26
  last_credit_refresh: datetime.datetime
27
  created_at: datetime.datetime
 
 
 
 
28
 
29
  model_config= ConfigDict(from_attributes= True)
30
 
 
25
  credits: int
26
  last_credit_refresh: datetime.datetime
27
  created_at: datetime.datetime
28
+ subscription_tier: str
29
+ messages_remaining: int
30
+ monthly_messages_used: int
31
+ last_billing_date: datetime.datetime
32
 
33
  model_config= ConfigDict(from_attributes= True)
34
 
frontend/src/App.tsx CHANGED
@@ -129,6 +129,10 @@ export default function App() {
129
  pictureUrl
130
  credits
131
  createdAt
 
 
 
 
132
  }
133
  }
134
  `);
@@ -567,10 +571,21 @@ export default function App() {
567
  query {
568
  me {
569
  credits
 
 
 
 
570
  }
571
  }
572
  `);
573
- setUser((prev: any) => prev ? { ...prev, credits: updatedProfile.me.credits } : null);
 
 
 
 
 
 
 
574
  } catch (e) {
575
  console.error('Refresh credits error:', e);
576
  }
 
129
  pictureUrl
130
  credits
131
  createdAt
132
+ subscriptionTier
133
+ messagesRemaining
134
+ monthlyMessagesUsed
135
+ lastBillingDate
136
  }
137
  }
138
  `);
 
571
  query {
572
  me {
573
  credits
574
+ subscriptionTier
575
+ messagesRemaining
576
+ monthlyMessagesUsed
577
+ lastBillingDate
578
  }
579
  }
580
  `);
581
+ setUser((prev: any) => prev ? {
582
+ ...prev,
583
+ credits: updatedProfile.me.credits,
584
+ subscriptionTier: updatedProfile.me.subscriptionTier,
585
+ messagesRemaining: updatedProfile.me.messagesRemaining,
586
+ monthlyMessagesUsed: updatedProfile.me.monthlyMessagesUsed,
587
+ lastBillingDate: updatedProfile.me.lastBillingDate
588
+ } : null);
589
  } catch (e) {
590
  console.error('Refresh credits error:', e);
591
  }
frontend/src/components/ChartChatbot.tsx CHANGED
@@ -30,15 +30,38 @@ export default function ChartChatbot({ ticker, markers, activeIndicators, user,
30
  const [input, setInput] = useState('');
31
  const [loading, setLoading] = useState(false);
32
 
33
- // Track chatbot trial message count
34
- const [trialUsed, setTrialUsed] = useState<boolean>(() => {
35
- return localStorage.getItem('quantiq_chatbot_trial_used') === 'true';
36
- });
 
 
 
 
 
 
 
 
37
 
38
  const messagesEndRef = useRef<HTMLDivElement>(null);
39
  const textareaRef = useRef<HTMLTextAreaElement>(null);
40
  const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000';
41
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  // Helper to format text markdown bold/italics, headers, bullets, and clean up stray symbols
43
  const formatMessageContent = (text: string) => {
44
  if (!text) return '';
@@ -48,59 +71,56 @@ export default function ChartChatbot({ ticker, markers, activeIndicators, user,
48
 
49
  // 1. Convert headers (e.g., #### Header or ### Header) to clean bold blocks
50
  const headerMatch = cleanedLine.match(/^(#{1,6})\s*(.*)$/);
51
- let isHeader = false;
52
  if (headerMatch) {
53
- cleanedLine = headerMatch[2];
54
- isHeader = true;
55
- }
56
-
57
- // 2. Convert bullet list points into clean lists
58
- const bulletMatch = cleanedLine.match(/^(\*|-)\s+(.*)$/);
59
- let isBullet = false;
60
- if (bulletMatch) {
61
- cleanedLine = bulletMatch[2];
62
- isBullet = true;
63
- }
64
-
65
- // 3. Parse inline bold (**text**) and italic (*text*) segments
66
- const parts = cleanedLine.split(/(\*\*.*?\*\*|\*.*?\*)/g);
67
- const parsedInline = parts.map((part, i) => {
68
- if (part.startsWith('**') && part.endsWith('**')) {
69
- return <strong key={i} style={{ color: '#fff', fontWeight: 700 }}>{part.slice(2, -2)}</strong>;
70
- } else if (part.startsWith('*') && part.endsWith('*')) {
71
- return <span key={i} style={{ color: 'var(--neon-cyan)', fontWeight: 500 }}>{part.slice(1, -1)}</span>;
72
- }
73
- // Remove any leftover stray asterisks in plain text segments
74
- return part.replace(/\*/g, '');
75
- });
76
-
77
- // Render line structure
78
- if (isHeader) {
79
  return (
80
- <h4 key={lineIdx} style={{ margin: '14px 0 6px 0', fontSize: '13px', fontWeight: 700, color: '#fff', lineHeight: 1.4 }}>
81
- {parsedInline}
82
  </h4>
83
  );
84
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
 
 
 
 
 
 
 
 
86
  if (isBullet) {
 
 
 
 
87
  return (
88
- <div key={lineIdx} style={{ display: 'flex', gap: '6px', alignItems: 'flex-start', margin: '4px 0 4px 8px' }}>
89
- <span style={{ color: 'var(--neon-cyan)', fontSize: '12px', lineHeight: '18px' }}>•</span>
90
- <div style={{ flex: 1 }}>{parsedInline}</div>
91
  </div>
92
  );
93
  }
94
-
95
  return (
96
- <p key={lineIdx} style={{ margin: cleanedLine.trim() === '' ? '0' : '0 0 8px 0', minHeight: cleanedLine.trim() === '' ? '8px' : 'auto' }}>
97
- {parsedInline}
98
  </p>
99
  );
100
  });
101
  };
102
 
103
- // Auto adjust textarea height on input change
104
  const adjustHeight = () => {
105
  if (textareaRef.current) {
106
  textareaRef.current.style.height = 'auto';
@@ -112,10 +132,6 @@ export default function ChartChatbot({ ticker, markers, activeIndicators, user,
112
  adjustHeight();
113
  }, [input]);
114
 
115
- // Check if user is a paid tier (Trader/Pro packs have > 10 credits) or is the test email
116
- const isPaidUser = user && (user.credits > 10 || user.email === 'karanshelar8775@gmail.com');
117
- const isTrialLocked = !isPaidUser && trialUsed;
118
-
119
  // Load chat history on ticker change
120
  useEffect(() => {
121
  const saved = localStorage.getItem(`quantiq_chat_history_${ticker}`);
@@ -143,7 +159,7 @@ export default function ChartChatbot({ ticker, markers, activeIndicators, user,
143
 
144
  const handleSendMessage = async (e?: React.FormEvent | React.KeyboardEvent) => {
145
  if (e) e.preventDefault();
146
- if (!input.trim() || loading || isTrialLocked) return;
147
 
148
  const userMessage = input.trim();
149
  setInput('');
@@ -152,7 +168,6 @@ export default function ChartChatbot({ ticker, markers, activeIndicators, user,
152
  }
153
  setLoading(true);
154
 
155
- // Add user message to log
156
  const updatedMessages = [...messages, { role: 'user' as const, content: userMessage }];
157
  setMessages(updatedMessages);
158
 
@@ -161,6 +176,7 @@ export default function ChartChatbot({ ticker, markers, activeIndicators, user,
161
  method: 'POST',
162
  headers: {
163
  'Content-Type': 'application/json',
 
164
  },
165
  body: JSON.stringify({
166
  ticker,
@@ -175,13 +191,16 @@ export default function ChartChatbot({ ticker, markers, activeIndicators, user,
175
  const data = await response.json();
176
  setMessages(prev => [...prev, { role: 'assistant', content: data.response }]);
177
 
178
- // If they are a free user, trigger the 1-time trial limit
179
- if (!isPaidUser) {
180
- localStorage.setItem('quantiq_chatbot_trial_used', 'true');
181
- setTrialUsed(true);
 
182
  }
183
  } else {
184
- setMessages(prev => [...prev, { role: 'assistant', content: "Sorry, I encountered an issue compiling the strategy feedback. Please try again." }]);
 
 
185
  }
186
  } catch (err) {
187
  console.error('Failed to chat with AI analyst:', err);
@@ -260,23 +279,21 @@ export default function ChartChatbot({ ticker, markers, activeIndicators, user,
260
  <span style={{ fontSize: '10px', color: 'var(--text-secondary)' }}>Real-time AI Copilot & Strategy Engine</span>
261
  </div>
262
 
263
- {/* Trial Badge */}
264
- {!isPaidUser && (
265
- <span
266
- style={{
267
- marginLeft: 'auto',
268
- fontSize: '9px',
269
- fontWeight: 700,
270
- background: trialUsed ? 'rgba(239, 68, 68, 0.15)' : 'rgba(16, 185, 129, 0.15)',
271
- color: trialUsed ? '#ef4444' : '#10b981',
272
- padding: '2px 6px',
273
- borderRadius: '4px',
274
- border: `1px solid ${trialUsed ? 'rgba(239, 68, 68, 0.25)' : 'rgba(16, 185, 129, 0.25)'}`
275
- }}
276
- >
277
- {trialUsed ? '0 Trials Left' : '1 Trial Left'}
278
- </span>
279
- )}
280
  </div>
281
 
282
  {/* Message Feed */}
@@ -365,7 +382,7 @@ export default function ChartChatbot({ ticker, markers, activeIndicators, user,
365
  background: 'rgba(10, 11, 20, 0.5)'
366
  }}
367
  >
368
- {isTrialLocked ? (
369
  <div
370
  className="animate-fade"
371
  style={{
@@ -377,10 +394,10 @@ export default function ChartChatbot({ ticker, markers, activeIndicators, user,
377
  }}
378
  >
379
  <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '6px', color: '#ef4444', fontSize: '12px', fontWeight: 700 }}>
380
- <Lock size={12} /> Trial Expired
381
  </div>
382
  <p style={{ margin: 0, fontSize: '10px', color: 'var(--text-secondary)', lineHeight: 1.4 }}>
383
- Unlock unlimited interactive chatbot sessions and custom indicator weights.
384
  </p>
385
  <button
386
  onClick={() => {
 
30
  const [input, setInput] = useState('');
31
  const [loading, setLoading] = useState(false);
32
 
33
+ // Local quota tracking synchronized with parent user prop
34
+ const [localRemaining, setLocalRemaining] = useState<number>(() => user?.messagesRemaining ?? 0);
35
+ const [localUsed, setLocalUsed] = useState<number>(() => user?.monthlyMessagesUsed ?? 0);
36
+ const [localTier, setLocalTier] = useState<string>(() => user?.subscriptionTier || 'free');
37
+
38
+ useEffect(() => {
39
+ if (user) {
40
+ setLocalRemaining(user.messagesRemaining ?? 0);
41
+ setLocalUsed(user.monthlyMessagesUsed ?? 0);
42
+ setLocalTier(user.subscriptionTier || 'free');
43
+ }
44
+ }, [user]);
45
 
46
  const messagesEndRef = useRef<HTMLDivElement>(null);
47
  const textareaRef = useRef<HTMLTextAreaElement>(null);
48
  const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000';
49
 
50
+ const isAdmin = user?.email === 'karanshelar8775@gmail.com';
51
+
52
+ const isLimitReached = !isAdmin && (
53
+ (localTier !== 'pro' && localRemaining <= 0) ||
54
+ (localTier === 'pro' && localUsed >= 100)
55
+ );
56
+
57
+ const getQuotaDisplay = () => {
58
+ if (isAdmin) return 'Unlimited (Admin)';
59
+ if (localTier === 'pro') {
60
+ return `${100 - localUsed} left`;
61
+ }
62
+ return `${localRemaining} left`;
63
+ };
64
+
65
  // Helper to format text markdown bold/italics, headers, bullets, and clean up stray symbols
66
  const formatMessageContent = (text: string) => {
67
  if (!text) return '';
 
71
 
72
  // 1. Convert headers (e.g., #### Header or ### Header) to clean bold blocks
73
  const headerMatch = cleanedLine.match(/^(#{1,6})\s*(.*)$/);
 
74
  if (headerMatch) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  return (
76
+ <h4 key={lineIdx} style={{ margin: '12px 0 6px', fontSize: '13px', fontWeight: 800, color: '#fff', borderBottom: '1px solid rgba(255,255,255,0.05)', paddingBottom: '4px' }}>
77
+ {headerMatch[2]}
78
  </h4>
79
  );
80
  }
81
+
82
+ // 2. Bold text matching **text**
83
+ const boldRegex = /\*\*(.*?)\*\*/g;
84
+ const parts = [];
85
+ let lastIndex = 0;
86
+ let match;
87
+
88
+ while ((match = boldRegex.exec(cleanedLine)) !== null) {
89
+ if (match.index > lastIndex) {
90
+ parts.push(cleanedLine.substring(lastIndex, match.index));
91
+ }
92
+ parts.push(<strong key={match.index} style={{ color: 'var(--neon-cyan)', fontWeight: 800 }}>{match[1]}</strong>);
93
+ lastIndex = boldRegex.lastIndex;
94
+ }
95
 
96
+ if (lastIndex < cleanedLine.length) {
97
+ parts.push(cleanedLine.substring(lastIndex));
98
+ }
99
+
100
+ const isBullet = cleanedLine.trim().startsWith('-') || cleanedLine.trim().startsWith('*');
101
+ const content = parts.length > 0 ? parts : cleanedLine;
102
+
103
  if (isBullet) {
104
+ // Strip bullet point indicator
105
+ const bulletText = typeof content === 'string'
106
+ ? content.replace(/^[\s-*]+/, '')
107
+ : content;
108
  return (
109
+ <div key={lineIdx} style={{ display: 'flex', gap: '6px', margin: '4px 0 4px 8px', fontSize: '12px', lineHeight: 1.5 }}>
110
+ <span style={{ color: 'var(--neon-cyan)' }}>•</span>
111
+ <span style={{ flex: 1 }}>{bulletText}</span>
112
  </div>
113
  );
114
  }
115
+
116
  return (
117
+ <p key={lineIdx} style={{ margin: cleanedLine.trim() === '' ? '8px 0' : '4px 0', minHeight: cleanedLine.trim() === '' ? '12px' : 'auto' }}>
118
+ {content}
119
  </p>
120
  );
121
  });
122
  };
123
 
 
124
  const adjustHeight = () => {
125
  if (textareaRef.current) {
126
  textareaRef.current.style.height = 'auto';
 
132
  adjustHeight();
133
  }, [input]);
134
 
 
 
 
 
135
  // Load chat history on ticker change
136
  useEffect(() => {
137
  const saved = localStorage.getItem(`quantiq_chat_history_${ticker}`);
 
159
 
160
  const handleSendMessage = async (e?: React.FormEvent | React.KeyboardEvent) => {
161
  if (e) e.preventDefault();
162
+ if (!input.trim() || loading || isLimitReached) return;
163
 
164
  const userMessage = input.trim();
165
  setInput('');
 
168
  }
169
  setLoading(true);
170
 
 
171
  const updatedMessages = [...messages, { role: 'user' as const, content: userMessage }];
172
  setMessages(updatedMessages);
173
 
 
176
  method: 'POST',
177
  headers: {
178
  'Content-Type': 'application/json',
179
+ 'Authorization': `Bearer ${localStorage.getItem('token')}`
180
  },
181
  body: JSON.stringify({
182
  ticker,
 
191
  const data = await response.json();
192
  setMessages(prev => [...prev, { role: 'assistant', content: data.response }]);
193
 
194
+ // Update local limits state
195
+ if (data.subscription_tier !== undefined) {
196
+ setLocalTier(data.subscription_tier);
197
+ setLocalRemaining(data.messages_remaining ?? 0);
198
+ setLocalUsed(data.monthly_messages_used ?? 0);
199
  }
200
  } else {
201
+ const errData = await response.json();
202
+ const errMessage = errData.detail || "Quota exhausted or query error. Check your subscription.";
203
+ setMessages(prev => [...prev, { role: 'assistant', content: `Sorry, I encountered an issue: ${errMessage}` }]);
204
  }
205
  } catch (err) {
206
  console.error('Failed to chat with AI analyst:', err);
 
279
  <span style={{ fontSize: '10px', color: 'var(--text-secondary)' }}>Real-time AI Copilot & Strategy Engine</span>
280
  </div>
281
 
282
+ {/* Quota display badge */}
283
+ <span
284
+ style={{
285
+ marginLeft: 'auto',
286
+ fontSize: '9px',
287
+ fontWeight: 700,
288
+ background: isLimitReached ? 'rgba(239, 68, 68, 0.15)' : 'rgba(16, 185, 129, 0.15)',
289
+ color: isLimitReached ? '#ef4444' : '#10b981',
290
+ padding: '2px 6px',
291
+ borderRadius: '4px',
292
+ border: `1px solid ${isLimitReached ? 'rgba(239, 68, 68, 0.25)' : 'rgba(16, 185, 129, 0.25)'}`
293
+ }}
294
+ >
295
+ {getQuotaDisplay()}
296
+ </span>
 
 
297
  </div>
298
 
299
  {/* Message Feed */}
 
382
  background: 'rgba(10, 11, 20, 0.5)'
383
  }}
384
  >
385
+ {isLimitReached ? (
386
  <div
387
  className="animate-fade"
388
  style={{
 
394
  }}
395
  >
396
  <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '6px', color: '#ef4444', fontSize: '12px', fontWeight: 700 }}>
397
+ <Lock size={12} /> Message Limit Reached
398
  </div>
399
  <p style={{ margin: 0, fontSize: '10px', color: 'var(--text-secondary)', lineHeight: 1.4 }}>
400
+ Unlock high-accuracy Wall Street strategies and indicator calculations.
401
  </p>
402
  <button
403
  onClick={() => {
frontend/src/pages/UpgradePage.tsx CHANGED
@@ -1,4 +1,5 @@
1
- import { Check, ArrowLeft } from 'lucide-react';
 
2
  import Logo from '../components/Logo';
3
 
4
  interface UpgradePageProps {
@@ -8,6 +9,8 @@ interface UpgradePageProps {
8
  }
9
 
10
  export default function UpgradePage({ user, onBack, onSelectPackage }: UpgradePageProps) {
 
 
11
  // Check if account age is under 3 days (3 * 24 * 60 * 60 * 1000 = 259200000 ms)
12
  const isNewUser = (() => {
13
  if (!user || !user.createdAt) return false;
@@ -21,6 +24,41 @@ export default function UpgradePage({ user, onBack, onSelectPackage }: UpgradePa
21
  }
22
  })();
23
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  return (
25
  <div className="upgrade-page-container" style={{ minHeight: '100vh', background: '#07090e', color: 'var(--text-primary)', padding: '40px 24px', position: 'relative', overflowY: 'auto' }}>
26
 
@@ -62,7 +100,7 @@ export default function UpgradePage({ user, onBack, onSelectPackage }: UpgradePa
62
  Plans that grow with you
63
  </h1>
64
  <p style={{ fontSize: '16px', color: 'var(--text-muted)', maxWidth: '600px', margin: '0 auto' }}>
65
- Whether you want to test the waters or build algorithmic trading strategies, choose the plan that fits your ambition.
66
  </p>
67
  </div>
68
 
@@ -82,7 +120,7 @@ export default function UpgradePage({ user, onBack, onSelectPackage }: UpgradePa
82
  <ul style={{ flex: 1, listStyle: 'none', padding: 0, margin: '0 0 32px', display: 'flex', flexDirection: 'column', gap: '14px', fontSize: '13px', color: 'var(--text-secondary)' }}>
83
  <li style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
84
  <Check size={16} style={{ color: 'var(--neon-cyan)', flexShrink: 0 }} />
85
- <span>5 weekly AI strategies</span>
86
  </li>
87
  <li style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
88
  <Check size={16} style={{ color: 'var(--neon-cyan)', flexShrink: 0 }} />
@@ -99,23 +137,24 @@ export default function UpgradePage({ user, onBack, onSelectPackage }: UpgradePa
99
  </ul>
100
 
101
  <button disabled style={{ width: '100%', padding: '12px', borderRadius: '10px', border: '1px solid rgba(255,255,255,0.05)', background: 'rgba(255,255,255,0.02)', color: 'var(--text-muted)', fontSize: '14px', fontWeight: 700, cursor: 'not-allowed', textAlign: 'center' }}>
102
- Active Plan
103
  </button>
104
  </div>
105
 
106
- {/* Card 1: 10 Credits */}
107
  <div className="pricing-full-card" onClick={() => onSelectPackage(500)} style={{ display: 'flex', flexDirection: 'column', padding: '32px 24px', background: 'rgba(255,255,255,0.02)', border: '1px solid var(--border-glass)', borderRadius: '16px', textAlign: 'left', transition: 'all 0.3s ease', cursor: 'pointer', position: 'relative' }}>
108
  <span style={{ fontSize: '12px', color: 'var(--neon-cyan)', fontWeight: 700, letterSpacing: '0.05em', textTransform: 'uppercase' }}>Analyst Pack</span>
109
- <h3 style={{ fontSize: '22px', fontWeight: 800, color: 'var(--text-primary)', marginTop: '8px' }}>10 Credits</h3>
110
  <p style={{ fontSize: '13px', color: 'var(--text-muted)', marginTop: '8px', minHeight: '38px' }}>Great for casual traders looking for reliable market strategies.</p>
111
  <div style={{ margin: '24px 0' }}>
112
  <span style={{ fontSize: '36px', fontWeight: 900, color: 'var(--text-primary)' }}>₹500</span>
 
113
  </div>
114
 
115
  <ul style={{ flex: 1, listStyle: 'none', padding: 0, margin: '0 0 32px', display: 'flex', flexDirection: 'column', gap: '14px', fontSize: '13px', color: 'var(--text-secondary)' }}>
116
  <li style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
117
  <Check size={16} style={{ color: 'var(--neon-cyan)', flexShrink: 0 }} />
118
- <span>10 Strategy Generations</span>
119
  </li>
120
  <li style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
121
  <Check size={16} style={{ color: 'var(--neon-cyan)', flexShrink: 0 }} />
@@ -132,24 +171,24 @@ export default function UpgradePage({ user, onBack, onSelectPackage }: UpgradePa
132
  </ul>
133
 
134
  <button className="insight-btn" style={{ width: '100%', padding: '12px', borderRadius: '10px', border: 'none', background: 'var(--neon-cyan)', color: 'var(--bg-black)', fontSize: '14px', fontWeight: 700, cursor: 'pointer', transition: 'all 0.2s ease' }}>
135
- Purchase Credits
136
  </button>
137
  </div>
138
 
139
- {/* Card 2: 50 Credits */}
140
- <div className="pricing-full-card hot-deal-card" onClick={() => onSelectPackage(1500)} style={{ display: 'flex', flexDirection: 'column', padding: '32px 24px', background: 'rgba(161, 84, 255, 0.05)', border: '1px solid rgba(161, 84, 255, 0.3)', borderRadius: '16px', textAlign: 'left', transition: 'all 0.3s ease', cursor: 'pointer', position: 'relative', boxShadow: '0 12px 48px rgba(161, 84, 255, 0.15)' }}>
141
- <div style={{ position: 'absolute', top: '-12px', right: '20px', background: 'var(--neon-violet)', color: 'var(--text-primary)', fontSize: '11px', fontWeight: 800, padding: '4px 10px', borderRadius: '20px', letterSpacing: '0.05em', textTransform: 'uppercase' }}>Most Popular</div>
142
  <span style={{ fontSize: '12px', color: 'var(--neon-violet)', fontWeight: 700, letterSpacing: '0.05em', textTransform: 'uppercase' }}>Trader Pack</span>
143
- <h3 style={{ fontSize: '22px', fontWeight: 800, color: 'var(--text-primary)', marginTop: '8px' }}>50 Credits</h3>
144
  <p style={{ fontSize: '13px', color: 'var(--text-muted)', marginTop: '8px', minHeight: '38px' }}>Designed for active traders seeking deep market intelligence.</p>
145
  <div style={{ margin: '24px 0' }}>
146
  <span style={{ fontSize: '36px', fontWeight: 900, color: 'var(--text-primary)' }}>₹1,500</span>
 
147
  </div>
148
 
149
  <ul style={{ flex: 1, listStyle: 'none', padding: 0, margin: '0 0 32px', display: 'flex', flexDirection: 'column', gap: '14px', fontSize: '13px', color: 'var(--text-secondary)' }}>
150
  <li style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
151
  <Check size={16} style={{ color: 'var(--neon-violet)', flexShrink: 0 }} />
152
- <span>50 Strategy Generations</span>
153
  </li>
154
  <li style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
155
  <Check size={16} style={{ color: 'var(--neon-violet)', flexShrink: 0 }} />
@@ -166,80 +205,114 @@ export default function UpgradePage({ user, onBack, onSelectPackage }: UpgradePa
166
  </ul>
167
 
168
  <button className="insight-btn" style={{ width: '100%', padding: '12px', borderRadius: '10px', border: 'none', background: 'var(--neon-violet)', color: 'var(--text-primary)', fontSize: '14px', fontWeight: 700, cursor: 'pointer', transition: 'all 0.2s ease', boxShadow: '0 4px 12px rgba(161, 84, 255, 0.3)' }}>
169
- Purchase Credits
170
  </button>
171
  </div>
172
 
173
- {/* Card 3: 100 Credits / Lifetime Offer */}
174
- {isNewUser ? (
175
- /* Special Lifetime Offer (Age <= 3 days) */
176
- <div className="pricing-full-card lifetime-deal-card" onClick={() => onSelectPackage(10500)} style={{ display: 'flex', flexDirection: 'column', padding: '32px 24px', background: 'rgba(0, 242, 254, 0.05)', border: '1px solid rgba(0, 242, 254, 0.5)', borderRadius: '16px', textAlign: 'left', transition: 'all 0.3s ease', cursor: 'pointer', position: 'relative', boxShadow: '0 12px 48px rgba(0, 242, 254, 0.2)' }}>
177
- <div style={{ position: 'absolute', top: '-12px', right: '20px', background: 'var(--neon-cyan)', color: 'var(--bg-black)', fontSize: '11px', fontWeight: 800, padding: '4px 10px', borderRadius: '20px', letterSpacing: '0.05em', textTransform: 'uppercase' }}>Limited Special Offer</div>
178
- <span style={{ fontSize: '12px', color: 'var(--neon-cyan)', fontWeight: 700, letterSpacing: '0.05em', textTransform: 'uppercase' }}>Lifetime Plan</span>
179
- <h3 style={{ fontSize: '22px', fontWeight: 800, color: 'var(--text-primary)', marginTop: '8px' }}>Unlimited AI</h3>
180
- <p style={{ fontSize: '13px', color: 'var(--text-muted)', marginTop: '8px', minHeight: '38px' }}>Ultimate freedom. Unlimited AI chat generations forever without ever paying again.</p>
181
- <div style={{ margin: '24px 0' }}>
182
- <span style={{ fontSize: '36px', fontWeight: 900, color: 'var(--text-primary)' }}>₹10,500</span>
183
- <span style={{ fontSize: '14px', color: 'var(--text-muted)', marginLeft: '4px' }}>once</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
  </div>
185
-
186
- <ul style={{ flex: 1, listStyle: 'none', padding: 0, margin: '0 0 32px', display: 'flex', flexDirection: 'column', gap: '14px', fontSize: '13px', color: 'var(--text-secondary)' }}>
187
- <li style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
188
- <Check size={16} style={{ color: 'var(--neon-cyan)', flexShrink: 0 }} />
189
- <strong style={{ color: 'var(--text-primary)' }}>Lifetime Unlimited AI chats</strong>
190
- </li>
191
- <li style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
192
- <Check size={16} style={{ color: 'var(--neon-cyan)', flexShrink: 0 }} />
193
- <span>Instant strategy reports</span>
194
- </li>
195
- <li style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
196
- <Check size={16} style={{ color: 'var(--neon-cyan)', flexShrink: 0 }} />
197
- <span>Unlimited watchlist & alerts</span>
198
- </li>
199
- <li style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
200
- <Check size={16} style={{ color: 'var(--neon-cyan)', flexShrink: 0 }} />
201
- <span>Exclusive developer features</span>
202
- </li>
203
- </ul>
204
-
205
- <button className="insight-btn" style={{ width: '100%', padding: '12px', borderRadius: '10px', border: 'none', background: 'linear-gradient(90deg, #00f2fe, #a154ff)', color: 'var(--bg-black)', fontSize: '14px', fontWeight: 800, cursor: 'pointer', transition: 'all 0.2s ease', boxShadow: '0 4px 12px rgba(0, 242, 254, 0.4)' }}>
206
- Claim Lifetime Deal
207
- </button>
208
  </div>
209
- ) : (
210
- /* Standard 100 Credits Card */
211
- <div className="pricing-full-card" onClick={() => onSelectPackage(2500)} style={{ display: 'flex', flexDirection: 'column', padding: '32px 24px', background: 'rgba(255,255,255,0.02)', border: '1px solid var(--border-glass)', borderRadius: '16px', textAlign: 'left', transition: 'all 0.3s ease', cursor: 'pointer', position: 'relative' }}>
212
- <span style={{ fontSize: '12px', color: 'var(--text-secondary)', fontWeight: 700, letterSpacing: '0.05em', textTransform: 'uppercase' }}>Pro Pack</span>
213
- <h3 style={{ fontSize: '22px', fontWeight: 800, color: 'var(--text-primary)', marginTop: '8px' }}>100 Credits</h3>
214
- <p style={{ fontSize: '13px', color: 'var(--text-muted)', marginTop: '8px', minHeight: '38px' }}>Best suited for quantitative researchers and serious day-traders.</p>
215
- <div style={{ margin: '24px 0' }}>
216
- <span style={{ fontSize: '36px', fontWeight: 900, color: 'var(--text-primary)' }}>₹2,500</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
217
  </div>
218
-
219
- <ul style={{ flex: 1, listStyle: 'none', padding: 0, margin: '0 0 32px', display: 'flex', flexDirection: 'column', gap: '14px', fontSize: '13px', color: 'var(--text-secondary)' }}>
220
- <li style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
221
- <Check size={16} style={{ color: 'var(--text-muted)', flexShrink: 0 }} />
222
- <span>100 Strategy Generations</span>
223
- </li>
224
- <li style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
225
- <Check size={16} style={{ color: 'var(--text-muted)', flexShrink: 0 }} />
226
- <span>Dedicated priority agent queue</span>
227
- </li>
228
- <li style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
229
- <Check size={16} style={{ color: 'var(--text-muted)', flexShrink: 0 }} />
230
- <span>Unlimited price alerts</span>
231
- </li>
232
- <li style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
233
- <Check size={16} style={{ color: 'var(--text-muted)', flexShrink: 0 }} />
234
- <span>Custom indicator weights</span>
235
- </li>
236
- </ul>
 
237
 
238
- <button className="insight-btn" style={{ width: '100%', padding: '12px', borderRadius: '10px', border: '1px solid rgba(255,255,255,0.1)', background: 'rgba(255,255,255,0.05)', color: 'var(--text-primary)', fontSize: '14px', fontWeight: 700, cursor: 'pointer', transition: 'all 0.2s ease' }}>
239
- Purchase Credits
240
- </button>
241
- </div>
242
- )}
243
 
244
  </div>
245
 
@@ -247,9 +320,9 @@ export default function UpgradePage({ user, onBack, onSelectPackage }: UpgradePa
247
  <p style={{ fontSize: '13px', color: 'var(--text-muted)', margin: 0 }}>
248
  Test Mode active. Pay securely using Razorpay Test UPI / Cards.
249
  </p>
250
- {isNewUser && (
251
  <p style={{ fontSize: '13px', color: 'var(--neon-cyan)', margin: 0, fontWeight: 700 }} className="offer-countdown">
252
- ⚡ Exclusive Lifetime Purchase offer ends in 3 days!
253
  </p>
254
  )}
255
  </div>
 
1
+ import { useState, useEffect } from 'react';
2
+ import { Check, ArrowLeft, Clock } from 'lucide-react';
3
  import Logo from '../components/Logo';
4
 
5
  interface UpgradePageProps {
 
9
  }
10
 
11
  export default function UpgradePage({ user, onBack, onSelectPackage }: UpgradePageProps) {
12
+ const [timeLeft, setTimeLeft] = useState<string>('');
13
+
14
  // Check if account age is under 3 days (3 * 24 * 60 * 60 * 1000 = 259200000 ms)
15
  const isNewUser = (() => {
16
  if (!user || !user.createdAt) return false;
 
24
  }
25
  })();
26
 
27
+ // Live countdown timer for the special offer
28
+ useEffect(() => {
29
+ if (!user || !user.createdAt) return;
30
+
31
+ const calculateTimeLeft = () => {
32
+ const createdDate = new Date(user.createdAt).getTime();
33
+ const expiryDate = createdDate + 3 * 24 * 60 * 60 * 1000; // 3 days limit
34
+ const difference = expiryDate - new Date().getTime();
35
+
36
+ if (difference <= 0) {
37
+ setTimeLeft('');
38
+ return;
39
+ }
40
+
41
+ const days = Math.floor(difference / (24 * 60 * 60 * 1000));
42
+ const hours = Math.floor((difference % (24 * 60 * 60 * 1000)) / (60 * 60 * 1000));
43
+ const minutes = Math.floor((difference % (60 * 60 * 1000)) / (60 * 1000));
44
+ const seconds = Math.floor((difference % (60 * 1000)) / 1000);
45
+
46
+ const dStr = days > 0 ? `${days}d ` : '';
47
+ const hStr = hours.toString().padStart(2, '0');
48
+ const mStr = minutes.toString().padStart(2, '0');
49
+ const sStr = seconds.toString().padStart(2, '0');
50
+
51
+ setTimeLeft(`${dStr}${hStr}h : ${mStr}m : ${sStr}s`);
52
+ };
53
+
54
+ calculateTimeLeft();
55
+ const interval = setInterval(calculateTimeLeft, 1000);
56
+ return () => clearInterval(interval);
57
+ }, [user]);
58
+
59
+ // Determine current active plan
60
+ const currentTier = user?.subscriptionTier || 'free';
61
+
62
  return (
63
  <div className="upgrade-page-container" style={{ minHeight: '100vh', background: '#07090e', color: 'var(--text-primary)', padding: '40px 24px', position: 'relative', overflowY: 'auto' }}>
64
 
 
100
  Plans that grow with you
101
  </h1>
102
  <p style={{ fontSize: '16px', color: 'var(--text-muted)', maxWidth: '600px', margin: '0 auto' }}>
103
+ Get access to wall-street level insights. Choose the plan that fits your ambition.
104
  </p>
105
  </div>
106
 
 
120
  <ul style={{ flex: 1, listStyle: 'none', padding: 0, margin: '0 0 32px', display: 'flex', flexDirection: 'column', gap: '14px', fontSize: '13px', color: 'var(--text-secondary)' }}>
121
  <li style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
122
  <Check size={16} style={{ color: 'var(--neon-cyan)', flexShrink: 0 }} />
123
+ <span><strong>3 AI Chat Messages</strong> (Lifetime limit)</span>
124
  </li>
125
  <li style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
126
  <Check size={16} style={{ color: 'var(--neon-cyan)', flexShrink: 0 }} />
 
137
  </ul>
138
 
139
  <button disabled style={{ width: '100%', padding: '12px', borderRadius: '10px', border: '1px solid rgba(255,255,255,0.05)', background: 'rgba(255,255,255,0.02)', color: 'var(--text-muted)', fontSize: '14px', fontWeight: 700, cursor: 'not-allowed', textAlign: 'center' }}>
140
+ {currentTier === 'free' ? 'Active Plan' : 'Free Tier'}
141
  </button>
142
  </div>
143
 
144
+ {/* Card 1: 10 Messages */}
145
  <div className="pricing-full-card" onClick={() => onSelectPackage(500)} style={{ display: 'flex', flexDirection: 'column', padding: '32px 24px', background: 'rgba(255,255,255,0.02)', border: '1px solid var(--border-glass)', borderRadius: '16px', textAlign: 'left', transition: 'all 0.3s ease', cursor: 'pointer', position: 'relative' }}>
146
  <span style={{ fontSize: '12px', color: 'var(--neon-cyan)', fontWeight: 700, letterSpacing: '0.05em', textTransform: 'uppercase' }}>Analyst Pack</span>
147
+ <h3 style={{ fontSize: '22px', fontWeight: 800, color: 'var(--text-primary)', marginTop: '8px' }}>10 Messages</h3>
148
  <p style={{ fontSize: '13px', color: 'var(--text-muted)', marginTop: '8px', minHeight: '38px' }}>Great for casual traders looking for reliable market strategies.</p>
149
  <div style={{ margin: '24px 0' }}>
150
  <span style={{ fontSize: '36px', fontWeight: 900, color: 'var(--text-primary)' }}>₹500</span>
151
+ <span style={{ fontSize: '14px', color: 'var(--text-muted)', marginLeft: '4px' }}>one-time</span>
152
  </div>
153
 
154
  <ul style={{ flex: 1, listStyle: 'none', padding: 0, margin: '0 0 32px', display: 'flex', flexDirection: 'column', gap: '14px', fontSize: '13px', color: 'var(--text-secondary)' }}>
155
  <li style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
156
  <Check size={16} style={{ color: 'var(--neon-cyan)', flexShrink: 0 }} />
157
+ <span><strong>10 AI Chat Messages</strong> (One-time)</span>
158
  </li>
159
  <li style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
160
  <Check size={16} style={{ color: 'var(--neon-cyan)', flexShrink: 0 }} />
 
171
  </ul>
172
 
173
  <button className="insight-btn" style={{ width: '100%', padding: '12px', borderRadius: '10px', border: 'none', background: 'var(--neon-cyan)', color: 'var(--bg-black)', fontSize: '14px', fontWeight: 700, cursor: 'pointer', transition: 'all 0.2s ease' }}>
174
+ {currentTier === 'analyst' ? 'Extend Balance' : 'Purchase Pack'}
175
  </button>
176
  </div>
177
 
178
+ {/* Card 2: 25 Messages */}
179
+ <div className="pricing-full-card" onClick={() => onSelectPackage(1500)} style={{ display: 'flex', flexDirection: 'column', padding: '32px 24px', background: 'rgba(255,255,255,0.02)', border: '1px solid var(--border-glass)', borderRadius: '16px', textAlign: 'left', transition: 'all 0.3s ease', cursor: 'pointer', position: 'relative' }}>
 
180
  <span style={{ fontSize: '12px', color: 'var(--neon-violet)', fontWeight: 700, letterSpacing: '0.05em', textTransform: 'uppercase' }}>Trader Pack</span>
181
+ <h3 style={{ fontSize: '22px', fontWeight: 800, color: 'var(--text-primary)', marginTop: '8px' }}>25 Messages</h3>
182
  <p style={{ fontSize: '13px', color: 'var(--text-muted)', marginTop: '8px', minHeight: '38px' }}>Designed for active traders seeking deep market intelligence.</p>
183
  <div style={{ margin: '24px 0' }}>
184
  <span style={{ fontSize: '36px', fontWeight: 900, color: 'var(--text-primary)' }}>₹1,500</span>
185
+ <span style={{ fontSize: '14px', color: 'var(--text-muted)', marginLeft: '4px' }}>one-time</span>
186
  </div>
187
 
188
  <ul style={{ flex: 1, listStyle: 'none', padding: 0, margin: '0 0 32px', display: 'flex', flexDirection: 'column', gap: '14px', fontSize: '13px', color: 'var(--text-secondary)' }}>
189
  <li style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
190
  <Check size={16} style={{ color: 'var(--neon-violet)', flexShrink: 0 }} />
191
+ <span><strong>25 AI Chat Messages</strong> (One-time)</span>
192
  </li>
193
  <li style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
194
  <Check size={16} style={{ color: 'var(--neon-violet)', flexShrink: 0 }} />
 
205
  </ul>
206
 
207
  <button className="insight-btn" style={{ width: '100%', padding: '12px', borderRadius: '10px', border: 'none', background: 'var(--neon-violet)', color: 'var(--text-primary)', fontSize: '14px', fontWeight: 700, cursor: 'pointer', transition: 'all 0.2s ease', boxShadow: '0 4px 12px rgba(161, 84, 255, 0.3)' }}>
208
+ {currentTier === 'trader' ? 'Extend Balance' : 'Purchase Pack'}
209
  </button>
210
  </div>
211
 
212
+ {/* Card 3: Pro Pack */}
213
+ <div
214
+ className="pricing-full-card hot-deal-card"
215
+ onClick={() => onSelectPackage(isNewUser && timeLeft ? 10000 : 15000)}
216
+ style={{
217
+ display: 'flex',
218
+ flexDirection: 'column',
219
+ padding: '32px 24px',
220
+ background: 'rgba(0, 242, 254, 0.03)',
221
+ border: '1px solid rgba(0, 242, 254, 0.4)',
222
+ borderRadius: '16px',
223
+ textAlign: 'left',
224
+ transition: 'all 0.3s ease',
225
+ cursor: 'pointer',
226
+ position: 'relative',
227
+ boxShadow: '0 12px 48px rgba(0, 242, 254, 0.15)'
228
+ }}
229
+ >
230
+ {isNewUser && timeLeft && (
231
+ <div
232
+ style={{
233
+ position: 'absolute',
234
+ top: '-14px',
235
+ right: '16px',
236
+ background: 'linear-gradient(90deg, #00f2fe, #a154ff)',
237
+ color: '#07090e',
238
+ fontSize: '11px',
239
+ fontWeight: 900,
240
+ padding: '4px 12px',
241
+ borderRadius: '20px',
242
+ display: 'flex',
243
+ alignItems: 'center',
244
+ gap: '6px',
245
+ boxShadow: '0 4px 12px rgba(0, 242, 254, 0.4)'
246
+ }}
247
+ >
248
+ <Clock size={12} />
249
+ <span>LIMITED OFFER</span>
250
  </div>
251
+ )}
252
+
253
+ <span style={{ fontSize: '12px', color: 'var(--neon-cyan)', fontWeight: 700, letterSpacing: '0.05em', textTransform: 'uppercase' }}>Pro Pack</span>
254
+ <h3 style={{ fontSize: '22px', fontWeight: 800, color: 'var(--text-primary)', marginTop: '8px' }}>100 Messages / mo</h3>
255
+ <p style={{ fontSize: '13px', color: 'var(--text-muted)', marginTop: '8px', minHeight: '38px' }}>For professional quantitative traders requiring maximum insight volume.</p>
256
+
257
+ <div style={{ margin: '24px 0', display: 'flex', alignItems: 'baseline', gap: '8px' }}>
258
+ {isNewUser && timeLeft ? (
259
+ <>
260
+ <span style={{ fontSize: '36px', fontWeight: 900, color: 'var(--neon-cyan)' }}>₹10,000</span>
261
+ <span style={{ fontSize: '16px', color: 'var(--text-muted)', textDecoration: 'line-through' }}>₹15,000</span>
262
+ </>
263
+ ) : (
264
+ <span style={{ fontSize: '36px', fontWeight: 900, color: 'var(--text-primary)' }}>₹15,000</span>
265
+ )}
266
+ <span style={{ fontSize: '14px', color: 'var(--text-muted)' }}>/ month</span>
 
 
 
 
 
 
 
267
  </div>
268
+
269
+ {/* Countdown Widget */}
270
+ {isNewUser && timeLeft && (
271
+ <div
272
+ style={{
273
+ background: 'rgba(0, 242, 254, 0.05)',
274
+ border: '1px solid rgba(0, 242, 254, 0.15)',
275
+ borderRadius: '8px',
276
+ padding: '8px 12px',
277
+ fontSize: '12px',
278
+ color: 'var(--neon-cyan)',
279
+ fontWeight: 700,
280
+ textAlign: 'center',
281
+ marginBottom: '20px',
282
+ display: 'flex',
283
+ alignItems: 'center',
284
+ justifyContent: 'center',
285
+ gap: '8px'
286
+ }}
287
+ >
288
+ <span>Closes in:</span>
289
+ <span style={{ fontFamily: 'monospace', fontSize: '13px' }}>{timeLeft}</span>
290
  </div>
291
+ )}
292
+
293
+ <ul style={{ flex: 1, listStyle: 'none', padding: 0, margin: '0 0 32px', display: 'flex', flexDirection: 'column', gap: '14px', fontSize: '13px', color: 'var(--text-secondary)' }}>
294
+ <li style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
295
+ <Check size={16} style={{ color: 'var(--neon-cyan)', flexShrink: 0 }} />
296
+ <span><strong>100 AI Chat Messages</strong> (Resets monthly)</span>
297
+ </li>
298
+ <li style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
299
+ <Check size={16} style={{ color: 'var(--neon-cyan)', flexShrink: 0 }} />
300
+ <span>Dedicated priority agent queue</span>
301
+ </li>
302
+ <li style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
303
+ <Check size={16} style={{ color: 'var(--neon-cyan)', flexShrink: 0 }} />
304
+ <span>Unlimited price alerts</span>
305
+ </li>
306
+ <li style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
307
+ <Check size={16} style={{ color: 'var(--neon-cyan)', flexShrink: 0 }} />
308
+ <span>Saved Strategy History</span>
309
+ </li>
310
+ </ul>
311
 
312
+ <button className="insight-btn" style={{ width: '100%', padding: '12px', borderRadius: '10px', border: 'none', background: 'linear-gradient(90deg, #00f2fe, #a154ff)', color: 'var(--bg-black)', fontSize: '14px', fontWeight: 800, cursor: 'pointer', transition: 'all 0.2s ease', boxShadow: '0 4px 12px rgba(0, 242, 254, 0.3)' }}>
313
+ {currentTier === 'pro' ? 'Active Plan' : 'Go Pro Now'}
314
+ </button>
315
+ </div>
 
316
 
317
  </div>
318
 
 
320
  <p style={{ fontSize: '13px', color: 'var(--text-muted)', margin: 0 }}>
321
  Test Mode active. Pay securely using Razorpay Test UPI / Cards.
322
  </p>
323
+ {isNewUser && timeLeft && (
324
  <p style={{ fontSize: '13px', color: 'var(--neon-cyan)', margin: 0, fontWeight: 700 }} className="offer-countdown">
325
+ ⚡ Exclusive 3-Day Pro Discount Offer active!
326
  </p>
327
  )}
328
  </div>