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 +38 -0
- backend/app/api/endpoints.py +37 -2
- backend/app/database/crud.py +34 -7
- backend/app/database/models.py +10 -0
- backend/app/graphql/schema.py +5 -3
- backend/app/schemas/schemas.py +4 -0
- frontend/src/App.tsx +16 -1
- frontend/src/components/ChartChatbot.tsx +88 -71
- frontend/src/pages/UpgradePage.tsx +155 -82
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(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 43 |
-
|
|
|
|
| 44 |
"""
|
| 45 |
now = datetime.datetime.now(datetime.timezone.utc)
|
| 46 |
-
|
| 47 |
|
| 48 |
-
if
|
| 49 |
-
|
| 50 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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 ? {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
//
|
| 34 |
-
const [
|
| 35 |
-
|
| 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: '
|
| 81 |
-
{
|
| 82 |
</h4>
|
| 83 |
);
|
| 84 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
if (isBullet) {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
return (
|
| 88 |
-
<div key={lineIdx} style={{ display: 'flex', gap: '6px',
|
| 89 |
-
<span style={{ color: 'var(--neon-cyan)'
|
| 90 |
-
<
|
| 91 |
</div>
|
| 92 |
);
|
| 93 |
}
|
| 94 |
-
|
| 95 |
return (
|
| 96 |
-
<p key={lineIdx} style={{ margin: cleanedLine.trim() === '' ? '0' : '
|
| 97 |
-
{
|
| 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 ||
|
| 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 |
-
//
|
| 179 |
-
if (!
|
| 180 |
-
|
| 181 |
-
|
|
|
|
| 182 |
}
|
| 183 |
} else {
|
| 184 |
-
|
|
|
|
|
|
|
| 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 |
-
{/*
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 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 |
-
{
|
| 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} />
|
| 381 |
</div>
|
| 382 |
<p style={{ margin: 0, fontSize: '10px', color: 'var(--text-secondary)', lineHeight: 1.4 }}>
|
| 383 |
-
Unlock
|
| 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 {
|
|
|
|
| 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 |
-
|
| 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>
|
| 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
|
| 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
|
| 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
|
| 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
|
| 136 |
</button>
|
| 137 |
</div>
|
| 138 |
|
| 139 |
-
{/* Card 2:
|
| 140 |
-
<div className="pricing-full-card
|
| 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' }}>
|
| 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>
|
| 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
|
| 170 |
</button>
|
| 171 |
</div>
|
| 172 |
|
| 173 |
-
{/* Card 3:
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 184 |
</div>
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
</
|
| 199 |
-
|
| 200 |
-
|
| 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 |
-
/*
|
| 211 |
-
|
| 212 |
-
<
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
</div>
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
</
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
</
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
</
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
</
|
| 236 |
-
</
|
|
|
|
| 237 |
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 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
|
| 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>
|