Add AI usage limits with progress bars and higher-limits request
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
Replace old Pro-only cost limits with unified system for all models: - Per-user limits: $0.15/day, $0.50/week, $1.00/month (degressive) - Global portal budget: $25/month (~100 PLN) tracked and displayed - Two progress bars in chat header: personal daily + global portal usage - Color-coded bars (green→yellow→red at 60%/90%) - Limit exceeded banner with "request higher limits" button - Backend endpoint logs requests for admin review - Flash model recommended as default (economical) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a3e5407a87
commit
2776a371b3
@ -6,7 +6,7 @@ AI Chat interface, API, and analytics.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from datetime import datetime, date
|
||||
from datetime import datetime, date, timedelta
|
||||
|
||||
from flask import render_template, request, redirect, url_for, flash, jsonify, session
|
||||
from flask_login import login_required, current_user
|
||||
@ -23,6 +23,101 @@ from utils.decorators import member_required
|
||||
# Logger
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# ============================================================
|
||||
# AI COST LIMITS
|
||||
# ============================================================
|
||||
# Global budget: 100 PLN/month (~$25) for all users except UNLIMITED_USERS.
|
||||
# Per-user limits are degressive (weekly < 7x daily, monthly < 4x weekly).
|
||||
|
||||
UNLIMITED_USERS = ['maciej.pienczyn@inpi.pl']
|
||||
|
||||
# Per-user limits in USD (both Flash and Pro combined)
|
||||
USER_DAILY_LIMIT = 0.15 # ~2-3 Flash queries/day
|
||||
USER_WEEKLY_LIMIT = 0.50 # ~9 Flash queries/week (not 7x daily)
|
||||
USER_MONTHLY_LIMIT = 1.00 # ~18 Flash queries/month (not 4x weekly)
|
||||
|
||||
|
||||
GLOBAL_MONTHLY_BUDGET = 25.00 # $25 = ~100 PLN
|
||||
|
||||
|
||||
def get_user_usage(user_id):
|
||||
"""Calculate user's AI cost usage for current day, week, and month, plus global usage."""
|
||||
now = datetime.now()
|
||||
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
week_start = today_start - timedelta(days=today_start.weekday()) # Monday
|
||||
month_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
# User's costs this month
|
||||
monthly_costs = db.query(AIAPICostLog).filter(
|
||||
AIAPICostLog.user_id == user_id,
|
||||
AIAPICostLog.timestamp >= month_start,
|
||||
AIAPICostLog.feature == 'ai_chat'
|
||||
).all()
|
||||
|
||||
daily_total = sum(float(c.total_cost or 0) for c in monthly_costs if c.timestamp >= today_start)
|
||||
weekly_total = sum(float(c.total_cost or 0) for c in monthly_costs if c.timestamp >= week_start)
|
||||
monthly_total = sum(float(c.total_cost or 0) for c in monthly_costs)
|
||||
|
||||
# Global portal usage this month (all users except unlimited)
|
||||
from database import User
|
||||
unlimited_ids = db.query(User.id).filter(User.email.in_(UNLIMITED_USERS)).all()
|
||||
unlimited_ids = [uid[0] for uid in unlimited_ids]
|
||||
|
||||
global_q = db.query(func.coalesce(func.sum(AIAPICostLog.total_cost), 0)).filter(
|
||||
AIAPICostLog.timestamp >= month_start,
|
||||
AIAPICostLog.feature == 'ai_chat'
|
||||
)
|
||||
if unlimited_ids:
|
||||
global_q = global_q.filter(~AIAPICostLog.user_id.in_(unlimited_ids))
|
||||
global_total = float(global_q.scalar() or 0)
|
||||
|
||||
return {
|
||||
'daily': round(daily_total, 4),
|
||||
'weekly': round(weekly_total, 4),
|
||||
'monthly': round(monthly_total, 4),
|
||||
'daily_limit': USER_DAILY_LIMIT,
|
||||
'weekly_limit': USER_WEEKLY_LIMIT,
|
||||
'monthly_limit': USER_MONTHLY_LIMIT,
|
||||
'daily_percent': round(min(daily_total / USER_DAILY_LIMIT * 100, 100), 1) if USER_DAILY_LIMIT > 0 else 0,
|
||||
'weekly_percent': round(min(weekly_total / USER_WEEKLY_LIMIT * 100, 100), 1) if USER_WEEKLY_LIMIT > 0 else 0,
|
||||
'monthly_percent': round(min(monthly_total / USER_MONTHLY_LIMIT * 100, 100), 1) if USER_MONTHLY_LIMIT > 0 else 0,
|
||||
'global_monthly': round(global_total, 4),
|
||||
'global_monthly_limit': GLOBAL_MONTHLY_BUDGET,
|
||||
'global_monthly_percent': round(min(global_total / GLOBAL_MONTHLY_BUDGET * 100, 100), 1) if GLOBAL_MONTHLY_BUDGET > 0 else 0,
|
||||
}
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
def check_user_limits(user_id, user_email):
|
||||
"""Check if user has exceeded any limit. Returns (exceeded, message) tuple."""
|
||||
if user_email in UNLIMITED_USERS:
|
||||
return False, None
|
||||
|
||||
usage = get_user_usage(user_id)
|
||||
|
||||
if usage['monthly'] >= USER_MONTHLY_LIMIT:
|
||||
return True, {
|
||||
'error': 'Osiągnięto miesięczny limit zapytań AI. Jeśli potrzebujesz więcej, skontaktuj się z administratorem.',
|
||||
'limit_exceeded': 'monthly',
|
||||
'usage': usage
|
||||
}
|
||||
if usage['weekly'] >= USER_WEEKLY_LIMIT:
|
||||
return True, {
|
||||
'error': 'Osiągnięto tygodniowy limit zapytań AI. Limit odnawia się w poniedziałek.',
|
||||
'limit_exceeded': 'weekly',
|
||||
'usage': usage
|
||||
}
|
||||
if usage['daily'] >= USER_DAILY_LIMIT:
|
||||
return True, {
|
||||
'error': 'Osiągnięto dzienny limit zapytań AI. Spróbuj ponownie jutro.',
|
||||
'limit_exceeded': 'daily',
|
||||
'usage': usage
|
||||
}
|
||||
return False, None
|
||||
|
||||
|
||||
# ============================================================
|
||||
# AI CHAT ROUTES
|
||||
@ -42,30 +137,26 @@ def chat():
|
||||
@login_required
|
||||
@member_required
|
||||
def chat_settings():
|
||||
"""Get or update chat settings (model selection, monthly cost)"""
|
||||
"""Get or update chat settings (model selection, usage limits)"""
|
||||
if request.method == 'GET':
|
||||
# Get current model from session or default to flash-lite
|
||||
model = session.get('chat_model', 'flash')
|
||||
is_unlimited = current_user.email in UNLIMITED_USERS
|
||||
|
||||
# Calculate monthly cost for current user
|
||||
monthly_cost = 0.0
|
||||
try:
|
||||
db = SessionLocal()
|
||||
# Get first day of current month
|
||||
first_day = datetime.now().replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
||||
costs = db.query(AIAPICostLog).filter(
|
||||
AIAPICostLog.user_id == current_user.id,
|
||||
AIAPICostLog.timestamp >= first_day
|
||||
).all()
|
||||
monthly_cost = sum(float(c.total_cost or 0) for c in costs)
|
||||
db.close()
|
||||
usage = get_user_usage(current_user.id)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error calculating monthly cost: {e}")
|
||||
logger.warning(f"Error calculating usage: {e}")
|
||||
usage = {'daily': 0, 'weekly': 0, 'monthly': 0,
|
||||
'daily_limit': USER_DAILY_LIMIT, 'weekly_limit': USER_WEEKLY_LIMIT,
|
||||
'monthly_limit': USER_MONTHLY_LIMIT,
|
||||
'daily_percent': 0, 'weekly_percent': 0, 'monthly_percent': 0}
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'model': model,
|
||||
'monthly_cost': round(monthly_cost, 4)
|
||||
'monthly_cost': round(usage['monthly'], 4),
|
||||
'usage': usage,
|
||||
'is_unlimited': is_unlimited
|
||||
})
|
||||
|
||||
# POST - update settings
|
||||
@ -93,6 +184,27 @@ def chat_settings():
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@bp.route('/api/chat/request-higher-limits', methods=['POST'])
|
||||
@login_required
|
||||
@member_required
|
||||
def chat_request_higher_limits():
|
||||
"""User requests higher AI limits — logs the request for admin review."""
|
||||
try:
|
||||
usage = get_user_usage(current_user.id)
|
||||
logger.info(
|
||||
f"HIGHER_LIMITS_REQUEST: User {current_user.id} ({current_user.name}, {current_user.email}) "
|
||||
f"requested higher AI limits. Current usage: daily=${usage['daily']}, "
|
||||
f"weekly=${usage['weekly']}, monthly=${usage['monthly']}"
|
||||
)
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': 'Twoje zgłoszenie zostało zarejestrowane. Administrator skontaktuje się z Tobą w sprawie indywidualnych limitów.'
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Error logging higher limits request: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@bp.route('/api/chat/start', methods=['POST'])
|
||||
@login_required
|
||||
@member_required
|
||||
@ -147,50 +259,10 @@ def chat_send_message(conversation_id):
|
||||
# Get model from request or session (flash = default with thinking, pro = premium)
|
||||
model_choice = data.get('model') or session.get('chat_model', 'flash')
|
||||
|
||||
# Check Pro model limits (Flash is free - no limits)
|
||||
if model_choice == 'pro':
|
||||
# Users without limits (admins)
|
||||
UNLIMITED_USERS = ['maciej.pienczyn@inpi.pl', 'artur.wiertel@waterm.pl']
|
||||
|
||||
if current_user.email not in UNLIMITED_USERS:
|
||||
# Check daily and monthly limits for Pro
|
||||
db_check = SessionLocal()
|
||||
try:
|
||||
# Daily limit: $2.00
|
||||
today_start = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
daily_costs = db_check.query(AIAPICostLog).filter(
|
||||
AIAPICostLog.user_id == current_user.id,
|
||||
AIAPICostLog.timestamp >= today_start,
|
||||
AIAPICostLog.model_name.like('%pro%')
|
||||
).all()
|
||||
daily_total = sum(float(c.total_cost or 0) for c in daily_costs)
|
||||
|
||||
if daily_total >= 2.0:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Osiągnięto dzienny limit Pro ($2.00). Spróbuj jutro lub użyj darmowego modelu Flash.',
|
||||
'limit_exceeded': 'daily',
|
||||
'daily_used': round(daily_total, 2)
|
||||
}), 429
|
||||
|
||||
# Monthly limit: $20.00
|
||||
month_start = datetime.now().replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
||||
monthly_costs = db_check.query(AIAPICostLog).filter(
|
||||
AIAPICostLog.user_id == current_user.id,
|
||||
AIAPICostLog.timestamp >= month_start,
|
||||
AIAPICostLog.model_name.like('%pro%')
|
||||
).all()
|
||||
monthly_total = sum(float(c.total_cost or 0) for c in monthly_costs)
|
||||
|
||||
if monthly_total >= 20.0:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Osiągnięto miesięczny limit Pro ($20.00). Użyj darmowego modelu Flash.',
|
||||
'limit_exceeded': 'monthly',
|
||||
'monthly_used': round(monthly_total, 2)
|
||||
}), 429
|
||||
finally:
|
||||
db_check.close()
|
||||
# Check usage limits (applies to all models)
|
||||
exceeded, limit_msg = check_user_limits(current_user.id, current_user.email)
|
||||
if exceeded:
|
||||
return jsonify({'success': False, **limit_msg}), 429
|
||||
|
||||
# Map model choice to actual model name and thinking level
|
||||
model_map = {
|
||||
|
||||
@ -937,32 +937,115 @@
|
||||
}
|
||||
|
||||
/* Monthly Cost Badge */
|
||||
.monthly-cost-badge {
|
||||
.usage-bars-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
margin-left: var(--spacing-sm);
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.usage-bar-mini {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
gap: 4px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.usage-bar-label {
|
||||
opacity: 0.7;
|
||||
min-width: 32px;
|
||||
text-align: right;
|
||||
color: rgba(255,255,255,0.8);
|
||||
}
|
||||
|
||||
.usage-bar-track {
|
||||
flex: 1;
|
||||
height: 6px;
|
||||
background: rgba(255,255,255,0.15);
|
||||
padding: 6px 12px;
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 12px;
|
||||
margin-left: var(--spacing-sm);
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
min-width: 50px;
|
||||
}
|
||||
|
||||
.monthly-cost-badge .cost-icon {
|
||||
font-size: 14px;
|
||||
.usage-bar-track.global {
|
||||
background: rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
.monthly-cost-badge .cost-label {
|
||||
opacity: 0.8;
|
||||
.usage-bar-fill {
|
||||
height: 100%;
|
||||
border-radius: 3px;
|
||||
background: #4ade80;
|
||||
transition: width 0.5s ease, background 0.3s ease;
|
||||
}
|
||||
|
||||
.monthly-cost-badge .cost-value {
|
||||
.usage-bar-fill.warning {
|
||||
background: #fbbf24;
|
||||
}
|
||||
|
||||
.usage-bar-fill.danger {
|
||||
background: #f87171;
|
||||
}
|
||||
|
||||
.usage-bar-fill.global {
|
||||
background: #60a5fa;
|
||||
}
|
||||
|
||||
.usage-bar-fill.global.warning {
|
||||
background: #fbbf24;
|
||||
}
|
||||
|
||||
.usage-bar-fill.global.danger {
|
||||
background: #f87171;
|
||||
}
|
||||
|
||||
.usage-bar-pct {
|
||||
min-width: 24px;
|
||||
text-align: right;
|
||||
font-weight: 600;
|
||||
color: #fef08a;
|
||||
color: rgba(255,255,255,0.9);
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
/* Limit exceeded banner */
|
||||
.limit-exceeded-banner {
|
||||
display: none;
|
||||
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
|
||||
color: #92400e;
|
||||
padding: 12px 16px;
|
||||
border-radius: var(--radius);
|
||||
margin: 8px 16px;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.limit-exceeded-banner.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.limit-exceeded-banner .limit-btn {
|
||||
display: inline-block;
|
||||
margin-top: 8px;
|
||||
padding: 6px 16px;
|
||||
background: #92400e;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: var(--radius);
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.limit-exceeded-banner .limit-btn:hover {
|
||||
background: #78350f;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.monthly-cost-badge .cost-label {
|
||||
.usage-bars-container {
|
||||
min-width: 80px;
|
||||
}
|
||||
.usage-bar-label {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@ -1449,10 +1532,10 @@
|
||||
<div class="thinking-option-header">
|
||||
<span class="thinking-option-icon">⚡</span>
|
||||
<span class="thinking-option-name">Flash</span>
|
||||
<span class="thinking-option-badge">Domyślny</span>
|
||||
<span class="thinking-option-badge">Zalecany</span>
|
||||
</div>
|
||||
<p class="thinking-option-desc">Thinking mode — szybki i inteligentny. Dla pytań o firmy, kontakty, strategie.</p>
|
||||
<span class="thinking-option-price">~$0.04/pytanie · 10 000 zapytań/dzień</span>
|
||||
<p class="thinking-option-desc">Szybki i inteligentny. Dla pytań o firmy, kontakty, strategie.</p>
|
||||
<span class="thinking-option-price">Ekonomiczny — zalecany do codziennego użytku</span>
|
||||
</div>
|
||||
<div class="thinking-option" data-model="pro" onclick="setModel('pro')">
|
||||
<div class="thinking-option-header">
|
||||
@ -1460,16 +1543,27 @@
|
||||
<span class="thinking-option-name">Pro</span>
|
||||
<span class="thinking-option-badge premium">Premium</span>
|
||||
</div>
|
||||
<p class="thinking-option-desc">Najlepsza analiza i rozumowanie. Dla złożonych raportów i rekomendacji.</p>
|
||||
<span class="thinking-option-price">~$0.20/pytanie · limit: $2/dzień</span>
|
||||
<p class="thinking-option-desc">Najlepsza analiza i rozumowanie. Dla złożonych raportów.</p>
|
||||
<span class="thinking-option-price">~4x droższy — szybciej zużywa limit</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Monthly cost display -->
|
||||
<div class="monthly-cost-badge" title="Twoje koszty AI w tym miesiącu">
|
||||
<span class="cost-icon">💰</span>
|
||||
<span class="cost-label">Ten miesiąc:</span>
|
||||
<span class="cost-value" id="monthlyCostDisplay">$0.00</span>
|
||||
<!-- Usage bars -->
|
||||
<div class="usage-bars-container" id="usageBarsContainer" title="Zużycie limitu AI">
|
||||
<div class="usage-bar-mini" title="Twój limit dzienny">
|
||||
<span class="usage-bar-label">Dziś</span>
|
||||
<div class="usage-bar-track">
|
||||
<div class="usage-bar-fill" id="dailyUsageBar" style="width: 0%"></div>
|
||||
</div>
|
||||
<span class="usage-bar-pct" id="dailyUsagePct">0%</span>
|
||||
</div>
|
||||
<div class="usage-bar-mini" title="Zużycie wszystkich użytkowników portalu w tym miesiącu">
|
||||
<span class="usage-bar-label">Portal</span>
|
||||
<div class="usage-bar-track global">
|
||||
<div class="usage-bar-fill global" id="globalUsageBar" style="width: 0%"></div>
|
||||
</div>
|
||||
<span class="usage-bar-pct" id="globalUsagePct">0%</span>
|
||||
</div>
|
||||
</div>
|
||||
<button class="model-info-btn" onclick="openModelInfoModal()" title="Informacje o modelu AI">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" width="16" height="16">
|
||||
@ -1485,6 +1579,13 @@
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Limit exceeded banner -->
|
||||
<div class="limit-exceeded-banner" id="limitExceededBanner">
|
||||
<span id="limitExceededMsg"></span>
|
||||
<br>
|
||||
<button class="limit-btn" onclick="requestHigherLimits()">Jestem zainteresowany wyższymi limitami</button>
|
||||
</div>
|
||||
|
||||
<!-- Modal z informacjami o modelu AI i historią rozwoju -->
|
||||
<div class="model-info-modal" id="modelInfoModal">
|
||||
<div class="model-info-content">
|
||||
@ -1505,8 +1606,12 @@
|
||||
</div>
|
||||
</div>
|
||||
<p class="model-description">
|
||||
<strong>Flash</strong> — Gemini 3 Flash z thinking mode, 10 000 zapytań/dzień.
|
||||
<strong>Pro</strong> — Gemini 3 Pro, najlepsza analiza dla złożonych pytań.
|
||||
<strong>Flash</strong> — Gemini 3 Flash z thinking mode. Zalecany do codziennego użytku.
|
||||
<strong>Pro</strong> — Gemini 3 Pro, najlepsza analiza. ~4x droższy, szybciej zużywa limit.
|
||||
</p>
|
||||
<p class="model-description" style="margin-top: 8px; font-size: 12px; color: var(--text-secondary);">
|
||||
Każdy użytkownik ma indywidualny limit zapytań (dzienny, tygodniowy, miesięczny).
|
||||
Jeśli potrzebujesz więcej, kliknij przycisk „Jestem zainteresowany wyższymi limitami".
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@ -1775,9 +1880,89 @@ async function saveModelPreference(model) {
|
||||
|
||||
function updateMonthlyCost(cost) {
|
||||
monthlyUsageCost += cost;
|
||||
const costDisplay = document.getElementById('monthlyCostDisplay');
|
||||
if (costDisplay) {
|
||||
costDisplay.textContent = '$' + monthlyUsageCost.toFixed(2);
|
||||
// Refresh usage bars after each message
|
||||
refreshUsageBars();
|
||||
}
|
||||
|
||||
function updateUsageBars(usage) {
|
||||
if (!usage) return;
|
||||
|
||||
// Daily bar (user)
|
||||
const dailyBar = document.getElementById('dailyUsageBar');
|
||||
const dailyPct = document.getElementById('dailyUsagePct');
|
||||
if (dailyBar && dailyPct) {
|
||||
const dp = Math.min(usage.daily_percent || 0, 100);
|
||||
dailyBar.style.width = dp + '%';
|
||||
dailyPct.textContent = Math.round(dp) + '%';
|
||||
dailyBar.className = 'usage-bar-fill' + (dp >= 90 ? ' danger' : dp >= 60 ? ' warning' : '');
|
||||
}
|
||||
|
||||
// Global bar (portal)
|
||||
const globalBar = document.getElementById('globalUsageBar');
|
||||
const globalPct = document.getElementById('globalUsagePct');
|
||||
if (globalBar && globalPct) {
|
||||
const gp = Math.min(usage.global_monthly_percent || 0, 100);
|
||||
globalBar.style.width = gp + '%';
|
||||
globalPct.textContent = Math.round(gp) + '%';
|
||||
globalBar.className = 'usage-bar-fill global' + (gp >= 90 ? ' danger' : gp >= 60 ? ' warning' : '');
|
||||
}
|
||||
|
||||
// Check if any limit is close or exceeded
|
||||
const maxPct = Math.max(usage.daily_percent || 0, usage.weekly_percent || 0, usage.monthly_percent || 0);
|
||||
if (maxPct >= 100) {
|
||||
showLimitBanner(usage);
|
||||
}
|
||||
}
|
||||
|
||||
function showLimitBanner(usage) {
|
||||
const banner = document.getElementById('limitExceededBanner');
|
||||
const msg = document.getElementById('limitExceededMsg');
|
||||
if (!banner || !msg) return;
|
||||
|
||||
let text = '';
|
||||
if ((usage.monthly_percent || 0) >= 100) {
|
||||
text = 'Osiągnięto miesięczny limit zapytań AI. Limit odnawia się 1. dnia kolejnego miesiąca.';
|
||||
} else if ((usage.weekly_percent || 0) >= 100) {
|
||||
text = 'Osiągnięto tygodniowy limit zapytań AI. Limit odnawia się w poniedziałek.';
|
||||
} else if ((usage.daily_percent || 0) >= 100) {
|
||||
text = 'Osiągnięto dzienny limit zapytań AI. Spróbuj ponownie jutro.';
|
||||
}
|
||||
|
||||
if (text) {
|
||||
msg.textContent = text;
|
||||
banner.classList.add('active');
|
||||
}
|
||||
}
|
||||
|
||||
async function requestHigherLimits() {
|
||||
try {
|
||||
const response = await fetch('/api/chat/request-higher-limits', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken }
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
const btn = document.querySelector('.limit-exceeded-banner .limit-btn');
|
||||
if (btn) {
|
||||
btn.textContent = 'Zgłoszenie wysłane';
|
||||
btn.disabled = true;
|
||||
btn.style.opacity = '0.6';
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error requesting higher limits:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshUsageBars() {
|
||||
try {
|
||||
const response = await fetch('/api/chat/settings');
|
||||
const data = await response.json();
|
||||
if (data.success && data.usage) {
|
||||
updateUsageBars(data.usage);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('Could not refresh usage bars');
|
||||
}
|
||||
}
|
||||
|
||||
@ -1857,13 +2042,13 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
loadModelSettings();
|
||||
});
|
||||
|
||||
// Load model settings and monthly cost from server
|
||||
// Load model settings and usage from server
|
||||
async function loadModelSettings() {
|
||||
try {
|
||||
const response = await fetch('/api/chat/settings');
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
// Always start with Flash (Gemini 3 Flash, thinking mode) - ignore saved preference
|
||||
// Always start with Flash - ignore saved preference
|
||||
currentModel = 'flash';
|
||||
const config = MODEL_CONFIG['flash'];
|
||||
document.getElementById('modelLabel').textContent = config.label;
|
||||
@ -1871,10 +2056,17 @@ async function loadModelSettings() {
|
||||
document.querySelectorAll('.thinking-option').forEach(opt => {
|
||||
opt.classList.toggle('active', opt.dataset.model === 'flash');
|
||||
});
|
||||
// Load monthly cost
|
||||
// Load usage bars
|
||||
if (data.monthly_cost !== undefined) {
|
||||
monthlyUsageCost = data.monthly_cost;
|
||||
document.getElementById('monthlyCostDisplay').textContent = '$' + monthlyUsageCost.toFixed(2);
|
||||
}
|
||||
if (data.usage) {
|
||||
updateUsageBars(data.usage);
|
||||
}
|
||||
// Hide usage bars for unlimited users
|
||||
if (data.is_unlimited) {
|
||||
const container = document.getElementById('usageBarsContainer');
|
||||
if (container) container.style.display = 'none';
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
@ -2219,8 +2411,16 @@ async function sendMessage() {
|
||||
|
||||
if (data.success) {
|
||||
addMessage('assistant', data.message, true, data.tech_info);
|
||||
// Reload conversations to update list
|
||||
loadConversations();
|
||||
// Update cost if available
|
||||
if (data.tech_info && data.tech_info.cost_usd) {
|
||||
updateMonthlyCost(data.tech_info.cost_usd);
|
||||
}
|
||||
} else if (data.limit_exceeded) {
|
||||
// Show limit banner and usage info
|
||||
if (data.usage) updateUsageBars(data.usage);
|
||||
showLimitBanner(data.usage || {daily_percent: 100});
|
||||
addMessage('assistant', data.error);
|
||||
} else {
|
||||
addMessage('assistant', 'Przepraszam, wystąpił błąd: ' + (data.error || 'Nieznany błąd'));
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user