feat(chat): Wybór modelu Flash/Pro zamiast Thinking Mode + koszt miesięczny
UI: - Dropdown wyboru modelu: Flash (szybki, $0.05) vs Pro (analiza, $0.20) - Wyświetlanie kosztu miesięcznego w headerze - Badge odpowiedzi pokazuje: model, czas, koszt Backend: - Endpoint /api/chat/settings obsługuje model i monthly_cost - NordaBizChatEngine przyjmuje parametr model - Koszt zapisywany w tech_info odpowiedzi Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
fa696b331f
commit
26db9a7cc9
80
app.py
80
app.py
@ -4517,33 +4517,51 @@ def chat():
|
||||
@csrf.exempt
|
||||
@login_required
|
||||
def chat_settings():
|
||||
"""Get or update chat settings (thinking level)"""
|
||||
"""Get or update chat settings (model selection, monthly cost)"""
|
||||
if request.method == 'GET':
|
||||
# Get current thinking level from session or default
|
||||
thinking_level = session.get('thinking_level', 'high')
|
||||
# Get current model from session or default to flash
|
||||
model = session.get('chat_model', 'flash')
|
||||
|
||||
# Calculate monthly cost for current user
|
||||
monthly_cost = 0.0
|
||||
try:
|
||||
from database import AIApiCost
|
||||
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(AIApiCost).filter(
|
||||
AIApiCost.user_id == current_user.id,
|
||||
AIApiCost.timestamp >= first_day
|
||||
).all()
|
||||
monthly_cost = sum(c.total_cost_usd or 0 for c in costs)
|
||||
db.close()
|
||||
except Exception as e:
|
||||
logger.warning(f"Error calculating monthly cost: {e}")
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'thinking_level': thinking_level
|
||||
'model': model,
|
||||
'monthly_cost': round(monthly_cost, 4)
|
||||
})
|
||||
|
||||
# POST - update settings
|
||||
try:
|
||||
data = request.get_json()
|
||||
thinking_level = data.get('thinking_level', 'high')
|
||||
model = data.get('model', 'flash')
|
||||
|
||||
# Validate thinking level
|
||||
valid_levels = ['minimal', 'low', 'medium', 'high']
|
||||
if thinking_level not in valid_levels:
|
||||
thinking_level = 'high'
|
||||
# Validate model
|
||||
valid_models = ['flash', 'pro']
|
||||
if model not in valid_models:
|
||||
model = 'flash'
|
||||
|
||||
# Store in session
|
||||
session['thinking_level'] = thinking_level
|
||||
session['chat_model'] = model
|
||||
|
||||
logger.info(f"User {current_user.id} set thinking_level to: {thinking_level}")
|
||||
logger.info(f"User {current_user.id} set chat_model to: {model}")
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'thinking_level': thinking_level
|
||||
'model': model
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
@ -4602,24 +4620,28 @@ def chat_send_message(conversation_id):
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
# Get thinking level from request or session
|
||||
thinking_level = data.get('thinking_level') or session.get('thinking_level', 'high')
|
||||
# Get model from request or session (flash = default, pro = premium)
|
||||
model_choice = data.get('model') or session.get('chat_model', 'flash')
|
||||
|
||||
chat_engine = NordaBizChatEngine()
|
||||
# Map model choice to actual model name
|
||||
model_map = {
|
||||
'flash': '3-flash',
|
||||
'pro': '3-pro'
|
||||
}
|
||||
model_key = model_map.get(model_choice, '3-flash')
|
||||
|
||||
chat_engine = NordaBizChatEngine(model=model_key)
|
||||
response = chat_engine.send_message(
|
||||
conversation_id=conversation_id,
|
||||
user_message=message,
|
||||
user_id=current_user.id,
|
||||
thinking_level=thinking_level
|
||||
thinking_level='minimal' if model_choice == 'flash' else 'high'
|
||||
)
|
||||
|
||||
# Get free tier usage stats for today
|
||||
free_tier_stats = get_free_tier_usage()
|
||||
|
||||
# Calculate theoretical cost (Gemini 2.0 Flash pricing)
|
||||
# Get actual cost from response
|
||||
tokens_in = response.tokens_input or 0
|
||||
tokens_out = response.tokens_output or 0
|
||||
theoretical_cost = (tokens_in / 1_000_000) * 0.075 + (tokens_out / 1_000_000) * 0.30
|
||||
actual_cost = response.cost_usd or 0.0
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
@ -4628,24 +4650,12 @@ def chat_send_message(conversation_id):
|
||||
'created_at': response.created_at.isoformat(),
|
||||
# Technical metadata
|
||||
'tech_info': {
|
||||
'model': gemini_service.get_gemini_service().model_name if gemini_service.get_gemini_service() else 'gemini-3-flash-preview',
|
||||
'thinking_level': thinking_level,
|
||||
'thinking_enabled': gemini_service.get_gemini_service().thinking_enabled if gemini_service.get_gemini_service() else True,
|
||||
'data_source': 'PostgreSQL (150 firm Norda Biznes)',
|
||||
'architecture': 'Full DB Context + Thinking Mode',
|
||||
'model': model_choice,
|
||||
'tokens_input': tokens_in,
|
||||
'tokens_output': tokens_out,
|
||||
'tokens_total': tokens_in + tokens_out,
|
||||
'latency_ms': response.latency_ms or 0,
|
||||
'theoretical_cost_usd': round(theoretical_cost, 6),
|
||||
'actual_cost_usd': 0.0, # Paid tier but tracked
|
||||
'free_tier': {
|
||||
'is_free': False,
|
||||
'daily_limit': 10000, # Gemini paid tier
|
||||
'requests_today': free_tier_stats['requests_today'],
|
||||
'tokens_today': free_tier_stats['tokens_today'],
|
||||
'remaining': max(0, 10000 - free_tier_stats['requests_today'])
|
||||
}
|
||||
'cost_usd': round(actual_cost, 6)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@ -86,17 +86,25 @@ class NordaBizChatEngine:
|
||||
Helps users find companies, services, and business partners.
|
||||
"""
|
||||
|
||||
def __init__(self, gemini_api_key: Optional[str] = None, use_global_service: bool = True):
|
||||
def __init__(self, gemini_api_key: Optional[str] = None, use_global_service: bool = True, model: str = None):
|
||||
"""
|
||||
Initialize Norda Biznes Chat Engine
|
||||
|
||||
Args:
|
||||
gemini_api_key: Google Gemini API key (uses env var if not provided)
|
||||
use_global_service: Use global gemini_service for automatic cost tracking (default: True)
|
||||
model: Model key ('3-flash', '3-pro') - if provided, creates new service with this model
|
||||
"""
|
||||
self.use_global_service = use_global_service
|
||||
self.requested_model = model
|
||||
|
||||
if use_global_service:
|
||||
if model:
|
||||
# Create new service with requested model
|
||||
from gemini_service import GeminiService
|
||||
self.gemini_service = GeminiService(model=model)
|
||||
self.model_name = self.gemini_service.model_name
|
||||
else:
|
||||
# Use global gemini_service for automatic cost tracking to ai_api_costs table
|
||||
self.gemini_service = gemini_service.get_gemini_service()
|
||||
# Get model name from global service (currently Gemini 3 Flash Preview)
|
||||
|
||||
@ -311,6 +311,11 @@
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.thinking-badge-cost {
|
||||
color: #f59e0b;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Klikalne linki jako kolorowe badge'y */
|
||||
.message-content a {
|
||||
display: inline-block;
|
||||
@ -724,6 +729,49 @@
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.thinking-option-badge.premium {
|
||||
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
|
||||
}
|
||||
|
||||
.thinking-option-price {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
color: #6b7280;
|
||||
margin-top: 4px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Monthly Cost Badge */
|
||||
.monthly-cost-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
background: rgba(255,255,255,0.15);
|
||||
padding: 6px 12px;
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 12px;
|
||||
margin-left: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.monthly-cost-badge .cost-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.monthly-cost-badge .cost-label {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.monthly-cost-badge .cost-value {
|
||||
font-weight: 600;
|
||||
color: #fef08a;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.monthly-cost-badge .cost-label {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.thinking-option-desc {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-xs);
|
||||
@ -1181,45 +1229,46 @@
|
||||
</button>
|
||||
<span style="font-size: 1.5rem;">🤖</span>
|
||||
<h1>NordaGPT</h1>
|
||||
<span class="chat-header-badge">Gemini 3</span>
|
||||
<!-- Thinking Mode Toggle -->
|
||||
<div class="thinking-toggle" id="thinkingToggle">
|
||||
<button class="thinking-btn" onclick="toggleThinkingDropdown()" title="Tryb rozumowania AI">
|
||||
<span class="thinking-icon">🧠</span>
|
||||
<span class="thinking-label" id="thinkingLabel">Wysoki</span>
|
||||
<!-- Model Selection Toggle -->
|
||||
<div class="thinking-toggle" id="modelToggle">
|
||||
<button class="thinking-btn" onclick="toggleModelDropdown()" title="Wybierz model AI">
|
||||
<span class="thinking-icon" id="modelIcon">⚡</span>
|
||||
<span class="thinking-label" id="modelLabel">Flash</span>
|
||||
<svg class="thinking-arrow" fill="none" stroke="currentColor" viewBox="0 0 24 24" width="12" height="12">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="thinking-dropdown" id="thinkingDropdown">
|
||||
<div class="thinking-dropdown" id="modelDropdown">
|
||||
<div class="thinking-dropdown-header">
|
||||
<strong>Tryb rozumowania AI</strong>
|
||||
<p>Określa głębokość analizy przed odpowiedzią</p>
|
||||
<strong>Model AI</strong>
|
||||
<p>Wybierz model dopasowany do pytania</p>
|
||||
</div>
|
||||
<div class="thinking-option" data-level="minimal" onclick="setThinkingLevel('minimal')">
|
||||
<div class="thinking-option active" data-model="flash" onclick="setModel('flash')">
|
||||
<div class="thinking-option-header">
|
||||
<span class="thinking-option-icon">⚡</span>
|
||||
<span class="thinking-option-name">Błyskawiczny</span>
|
||||
</div>
|
||||
<p class="thinking-option-desc">Najszybsze odpowiedzi. Dla prostych pytań typu "kto?", "gdzie?".</p>
|
||||
</div>
|
||||
<div class="thinking-option" data-level="low" onclick="setThinkingLevel('low')">
|
||||
<div class="thinking-option-header">
|
||||
<span class="thinking-option-icon">🚀</span>
|
||||
<span class="thinking-option-name">Szybki</span>
|
||||
</div>
|
||||
<p class="thinking-option-desc">Zrównoważony. Dobre dla większości pytań o firmy i usługi.</p>
|
||||
</div>
|
||||
<div class="thinking-option active" data-level="high" onclick="setThinkingLevel('high')">
|
||||
<div class="thinking-option-header">
|
||||
<span class="thinking-option-icon">🧠</span>
|
||||
<span class="thinking-option-name">Głęboki</span>
|
||||
<span class="thinking-option-name">Gemini Flash</span>
|
||||
<span class="thinking-option-badge">Domyślny</span>
|
||||
</div>
|
||||
<p class="thinking-option-desc">Maksymalna analiza. Dla złożonych pytań, rekomendacji, strategii.</p>
|
||||
<p class="thinking-option-desc">Szybki i ekonomiczny. Dla prostych pytań o firmy, kontakty, wydarzenia.</p>
|
||||
<span class="thinking-option-price">~$0.05/pytanie</span>
|
||||
</div>
|
||||
<div class="thinking-option" data-model="pro" onclick="setModel('pro')">
|
||||
<div class="thinking-option-header">
|
||||
<span class="thinking-option-icon">🧠</span>
|
||||
<span class="thinking-option-name">Gemini Pro</span>
|
||||
<span class="thinking-option-badge premium">Premium</span>
|
||||
</div>
|
||||
<p class="thinking-option-desc">Głęboka analiza. Dla złożonych pytań, strategii, rekomendacji.</p>
|
||||
<span class="thinking-option-price">~$0.20/pytanie</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>
|
||||
</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">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
@ -1431,57 +1480,67 @@
|
||||
// NordaGPT Chat - State
|
||||
let currentConversationId = null;
|
||||
let conversations = [];
|
||||
let currentThinkingLevel = 'high'; // Default thinking level
|
||||
let currentModel = 'flash'; // Default model (flash = ekonomiczny)
|
||||
let monthlyUsageCost = 0; // Koszt miesięczny użytkownika
|
||||
|
||||
// ============================================
|
||||
// Thinking Mode Toggle Functions
|
||||
// Model Selection Toggle Functions
|
||||
// ============================================
|
||||
const THINKING_LABELS = {
|
||||
'minimal': 'Błyskawiczny',
|
||||
'low': 'Szybki',
|
||||
'high': 'Głęboki'
|
||||
const MODEL_CONFIG = {
|
||||
'flash': { label: 'Flash', icon: '⚡', desc: 'Szybki' },
|
||||
'pro': { label: 'Pro', icon: '🧠', desc: 'Analiza' }
|
||||
};
|
||||
|
||||
function toggleThinkingDropdown() {
|
||||
const toggle = document.getElementById('thinkingToggle');
|
||||
function toggleModelDropdown() {
|
||||
const toggle = document.getElementById('modelToggle');
|
||||
toggle.classList.toggle('open');
|
||||
}
|
||||
|
||||
function setThinkingLevel(level) {
|
||||
currentThinkingLevel = level;
|
||||
function setModel(model) {
|
||||
currentModel = model;
|
||||
const config = MODEL_CONFIG[model];
|
||||
|
||||
// Update UI
|
||||
document.getElementById('thinkingLabel').textContent = THINKING_LABELS[level] || level;
|
||||
document.getElementById('modelLabel').textContent = config.label;
|
||||
document.getElementById('modelIcon').textContent = config.icon;
|
||||
|
||||
// Update active state
|
||||
document.querySelectorAll('.thinking-option').forEach(opt => {
|
||||
opt.classList.toggle('active', opt.dataset.level === level);
|
||||
opt.classList.toggle('active', opt.dataset.model === model);
|
||||
});
|
||||
|
||||
// Close dropdown
|
||||
document.getElementById('thinkingToggle').classList.remove('open');
|
||||
document.getElementById('modelToggle').classList.remove('open');
|
||||
|
||||
// Save preference to server
|
||||
saveThinkingPreference(level);
|
||||
saveModelPreference(model);
|
||||
|
||||
console.log('Thinking level set to:', level);
|
||||
console.log('Model set to:', model);
|
||||
}
|
||||
|
||||
async function saveThinkingPreference(level) {
|
||||
async function saveModelPreference(model) {
|
||||
try {
|
||||
await fetch('/api/chat/settings', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ thinking_level: level })
|
||||
body: JSON.stringify({ model: model })
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to save thinking preference:', error);
|
||||
console.error('Failed to save model preference:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Close thinking dropdown when clicking outside
|
||||
function updateMonthlyCost(cost) {
|
||||
monthlyUsageCost += cost;
|
||||
const costDisplay = document.getElementById('monthlyCostDisplay');
|
||||
if (costDisplay) {
|
||||
costDisplay.textContent = '$' + monthlyUsageCost.toFixed(2);
|
||||
}
|
||||
}
|
||||
|
||||
// Close model dropdown when clicking outside
|
||||
document.addEventListener('click', function(e) {
|
||||
const toggle = document.getElementById('thinkingToggle');
|
||||
const toggle = document.getElementById('modelToggle');
|
||||
if (toggle && !toggle.contains(e.target)) {
|
||||
toggle.classList.remove('open');
|
||||
}
|
||||
@ -1552,23 +1611,33 @@ document.addEventListener('click', function(e) {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadConversations();
|
||||
autoResizeTextarea();
|
||||
loadThinkingSettings();
|
||||
loadModelSettings();
|
||||
});
|
||||
|
||||
// Load thinking settings from server
|
||||
async function loadThinkingSettings() {
|
||||
// Load model settings and monthly cost from server
|
||||
async function loadModelSettings() {
|
||||
try {
|
||||
const response = await fetch('/api/chat/settings');
|
||||
const data = await response.json();
|
||||
if (data.success && data.thinking_level) {
|
||||
currentThinkingLevel = data.thinking_level;
|
||||
document.getElementById('thinkingLabel').textContent = THINKING_LABELS[data.thinking_level] || data.thinking_level;
|
||||
if (data.success) {
|
||||
// Load model preference
|
||||
if (data.model) {
|
||||
currentModel = data.model;
|
||||
const config = MODEL_CONFIG[data.model] || MODEL_CONFIG['flash'];
|
||||
document.getElementById('modelLabel').textContent = config.label;
|
||||
document.getElementById('modelIcon').textContent = config.icon;
|
||||
document.querySelectorAll('.thinking-option').forEach(opt => {
|
||||
opt.classList.toggle('active', opt.dataset.level === data.thinking_level);
|
||||
opt.classList.toggle('active', opt.dataset.model === data.model);
|
||||
});
|
||||
}
|
||||
// Load monthly cost
|
||||
if (data.monthly_cost !== undefined) {
|
||||
monthlyUsageCost = data.monthly_cost;
|
||||
document.getElementById('monthlyCostDisplay').textContent = '$' + monthlyUsageCost.toFixed(2);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Using default thinking level:', currentThinkingLevel);
|
||||
console.log('Using default model:', currentModel);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1769,13 +1838,13 @@ async function sendMessage() {
|
||||
}
|
||||
}
|
||||
|
||||
// Send message with thinking level
|
||||
// Send message with model selection
|
||||
const response = await fetch(`/api/chat/${currentConversationId}/message`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
message: message,
|
||||
thinking_level: currentThinkingLevel
|
||||
model: currentModel
|
||||
})
|
||||
});
|
||||
const data = await response.json();
|
||||
@ -1826,33 +1895,35 @@ function addMessage(role, content, animate = true, techInfo = null) {
|
||||
contentDiv.className = 'message-content';
|
||||
contentDiv.innerHTML = formatMessage(content);
|
||||
|
||||
// Add thinking info badge for AI responses
|
||||
// Add response info badge for AI responses (model, time, cost)
|
||||
if (role === 'assistant' && techInfo) {
|
||||
const thinkingBadge = document.createElement('div');
|
||||
thinkingBadge.className = 'thinking-info-badge';
|
||||
const infoBadge = document.createElement('div');
|
||||
infoBadge.className = 'thinking-info-badge';
|
||||
|
||||
const thinkingLevel = techInfo.thinking_level || 'high';
|
||||
const modelName = techInfo.model || 'flash';
|
||||
const latencyMs = techInfo.latency_ms || 0;
|
||||
const latencySec = (latencyMs / 1000).toFixed(1);
|
||||
const costUsd = techInfo.cost_usd || 0;
|
||||
|
||||
// Labels with quality descriptions to show value of deeper thinking
|
||||
const levelLabels = {
|
||||
'minimal': '⚡ Błyskawiczny',
|
||||
'low': '🚀 Szybki',
|
||||
'medium': '⚖️ Zbalansowany',
|
||||
'high': '🧠 Głęboka analiza'
|
||||
// Model labels
|
||||
const modelLabels = {
|
||||
'flash': '⚡ Flash',
|
||||
'pro': '🧠 Pro',
|
||||
'gemini-3-flash-preview': '⚡ Flash',
|
||||
'gemini-3-pro-preview': '🧠 Pro'
|
||||
};
|
||||
const levelDescriptions = {
|
||||
'minimal': 'szybka odpowiedź',
|
||||
'low': 'zwięzła analiza',
|
||||
'medium': 'przemyślana odpowiedź',
|
||||
'high': 'dogłębna analiza z weryfikacją'
|
||||
};
|
||||
const levelLabel = levelLabels[thinkingLevel] || thinkingLevel;
|
||||
const levelDesc = levelDescriptions[thinkingLevel] || '';
|
||||
const modelLabel = modelLabels[modelName] || modelName;
|
||||
|
||||
thinkingBadge.innerHTML = `<span class="thinking-badge-level">${levelLabel}</span> · <span class="thinking-badge-desc">${levelDesc}</span> · <span class="thinking-badge-time">${latencySec}s</span>`;
|
||||
contentDiv.appendChild(thinkingBadge);
|
||||
// Format cost
|
||||
const costStr = costUsd > 0 ? `$${costUsd.toFixed(4)}` : '$0.00';
|
||||
|
||||
infoBadge.innerHTML = `<span class="thinking-badge-level">${modelLabel}</span> · <span class="thinking-badge-time">${latencySec}s</span> · <span class="thinking-badge-cost">${costStr}</span>`;
|
||||
contentDiv.appendChild(infoBadge);
|
||||
|
||||
// Update monthly cost if cost provided
|
||||
if (costUsd > 0) {
|
||||
updateMonthlyCost(costUsd);
|
||||
}
|
||||
}
|
||||
|
||||
messageDiv.appendChild(avatar);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user