fix(ui+billing): dark theme for session page + no double-charge on restart
FocusGroupSession.tsx: replace bg-slate-50 with bg-background so the page matches the rest of the dark UI; swap remaining text-slate-* for muted tokens. focus_group_ai.py: before deducting 40 cr for an AI mode run, check if this focus group already has a charge within the last 4 hours. Skip deduction if found — prevents double-billing when the server restarts mid-session. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
56efcc38f5
commit
0a419eeee1
2 changed files with 49 additions and 31 deletions
|
|
@ -35,6 +35,7 @@ from app.models.credit_transaction import CreditTransaction
|
|||
from app.models.app_settings import get_settings
|
||||
from app.utils.rate_limiter import rate_limit
|
||||
from app.utils import active_required, with_user_context
|
||||
from app.db import get_db
|
||||
|
||||
# Create the blueprint
|
||||
focus_group_ai_bp = Blueprint('focus_group_ai', __name__)
|
||||
|
|
@ -765,26 +766,43 @@ async def start_autonomous_conversation(focus_group_id):
|
|||
run_user_id = get_jwt_identity()
|
||||
settings = await get_settings()
|
||||
run_cost = settings.get("run_cost", 40)
|
||||
user_data = await User.find_by_id(run_user_id)
|
||||
balance = (user_data or {}).get("credits_balance", 0)
|
||||
if balance < run_cost:
|
||||
return jsonify({
|
||||
"error": "Insufficient credits",
|
||||
"message": f"You need {run_cost} credits to run a focus group session. Current balance: {balance}.",
|
||||
"credits_required": run_cost,
|
||||
"credits_balance": balance,
|
||||
}), 402
|
||||
new_balance = await User.deduct_credits(run_user_id, run_cost)
|
||||
if new_balance is None:
|
||||
return jsonify({"error": "Insufficient credits", "message": "Credit balance changed. Please try again."}), 402
|
||||
await CreditTransaction.record(
|
||||
user_id=run_user_id,
|
||||
tx_type="debit",
|
||||
amount=-run_cost,
|
||||
balance_after=new_balance,
|
||||
description=f"Focus group session run",
|
||||
ref={"focus_group_id": focus_group_id},
|
||||
)
|
||||
|
||||
# Skip charge if this focus group was already charged within the last 4 hours
|
||||
# (handles server restarts and error-triggered restarts without double-billing)
|
||||
from datetime import datetime, timezone, timedelta
|
||||
db = await get_db()
|
||||
recent_cutoff = datetime.now(timezone.utc) - timedelta(hours=4)
|
||||
recent_charge = await db.credit_transactions.find_one({
|
||||
"user_id": run_user_id,
|
||||
"ref.focus_group_id": focus_group_id,
|
||||
"description": "Focus group session run",
|
||||
"ts": {"$gte": recent_cutoff},
|
||||
})
|
||||
|
||||
if recent_charge is None:
|
||||
user_data = await User.find_by_id(run_user_id)
|
||||
balance = (user_data or {}).get("credits_balance", 0)
|
||||
if balance < run_cost:
|
||||
return jsonify({
|
||||
"error": "Insufficient credits",
|
||||
"message": f"You need {run_cost} credits to run a focus group session. Current balance: {balance}.",
|
||||
"credits_required": run_cost,
|
||||
"credits_balance": balance,
|
||||
}), 402
|
||||
new_balance = await User.deduct_credits(run_user_id, run_cost)
|
||||
if new_balance is None:
|
||||
return jsonify({"error": "Insufficient credits", "message": "Credit balance changed. Please try again."}), 402
|
||||
await CreditTransaction.record(
|
||||
user_id=run_user_id,
|
||||
tx_type="debit",
|
||||
amount=-run_cost,
|
||||
balance_after=new_balance,
|
||||
description="Focus group session run",
|
||||
ref={"focus_group_id": focus_group_id},
|
||||
)
|
||||
current_app.logger.info(f"Charged {run_cost} credits for focus group {focus_group_id}")
|
||||
else:
|
||||
current_app.logger.info(f"Focus group {focus_group_id} already charged within 4h window — skipping deduction")
|
||||
|
||||
# Create autonomous conversation controller
|
||||
current_app.logger.info("Creating AutonomousConversationController...")
|
||||
|
|
|
|||
|
|
@ -1762,13 +1762,13 @@ const FocusGroupSession = () => {
|
|||
// Show loading state while fetching
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-50 pt-20 pb-16 px-4">
|
||||
<div className="min-h-screen bg-background pt-20 pb-16 px-4">
|
||||
|
||||
<div className="max-w-7xl mx-auto text-center py-12">
|
||||
<div className="flex justify-center items-center">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
||||
</div>
|
||||
<p className="mt-4 text-slate-600">Loading focus group...</p>
|
||||
<p className="mt-4 text-muted-foreground">Loading focus group...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -1777,11 +1777,11 @@ const FocusGroupSession = () => {
|
|||
// Show error state if not found after loading
|
||||
if (!focusGroup) {
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-50 pt-20 pb-16 px-4">
|
||||
<div className="min-h-screen bg-background pt-20 pb-16 px-4">
|
||||
|
||||
<div className="max-w-7xl mx-auto text-center py-12">
|
||||
<h1 className="text-2xl font-bold">Focus group not found</h1>
|
||||
<p className="mt-2 text-slate-600">We couldn't find the focus group you're looking for.</p>
|
||||
<p className="mt-2 text-muted-foreground">We couldn't find the focus group you're looking for.</p>
|
||||
<Button
|
||||
onClick={() => navigate('/focus-groups')}
|
||||
className="mt-4"
|
||||
|
|
@ -1794,7 +1794,7 @@ const FocusGroupSession = () => {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-50">
|
||||
<div className="min-h-screen bg-background">
|
||||
|
||||
|
||||
{/* WebSocket Connection Status Bar */}
|
||||
|
|
@ -1887,7 +1887,7 @@ const FocusGroupSession = () => {
|
|||
<h1 className="font-sf text-2xl font-bold text-foreground">{focusGroup.name}</h1>
|
||||
<p className="text-muted-foreground">{new Date(focusGroup.date).toLocaleString()}</p>
|
||||
<div className="flex items-center mt-1">
|
||||
<Bot className="h-3 w-3 text-slate-500 mr-1" />
|
||||
<Bot className="h-3 w-3 text-muted-foreground mr-1" />
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{focusGroup.llm_model === 'gpt-5.4-mini' ? 'GPT-5.4 Mini' : 'GPT-5.4'}
|
||||
</Badge>
|
||||
|
|
@ -2019,7 +2019,7 @@ const FocusGroupSession = () => {
|
|||
<TabsContent value="chat" className="m-0 flex-1 flex flex-col h-0">
|
||||
{messages.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 space-y-4">
|
||||
<p className="text-lg text-slate-600">No messages yet. Start the session to begin the discussion.</p>
|
||||
<p className="text-lg text-muted-foreground">No messages yet. Start the session to begin the discussion.</p>
|
||||
<Button
|
||||
onClick={startSession}
|
||||
size="lg"
|
||||
|
|
@ -2261,7 +2261,7 @@ const FocusGroupSession = () => {
|
|||
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Bot className="h-4 w-4 text-slate-500" />
|
||||
<Bot className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-sm font-medium">Current Model:</span>
|
||||
<Badge variant="secondary">
|
||||
{focusGroup?.llm_model === 'gpt-5.4-mini' ? 'GPT-5.4 Mini' : 'GPT-5.4'}
|
||||
|
|
@ -2300,7 +2300,7 @@ const FocusGroupSession = () => {
|
|||
<SelectItem value="high">High - Deep reasoning</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-slate-600 mt-1">
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Controls how much time GPT-5.4 spends thinking before responding
|
||||
</p>
|
||||
<p className="text-xs text-amber-600 font-medium mt-1">
|
||||
|
|
@ -2321,7 +2321,7 @@ Controls how thoroughly GPT-5.4 thinks and how detailed responses are
|
|||
<SelectItem value="high">High - Detailed responses</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-slate-600 mt-1">
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Controls how detailed and lengthy GPT-5.4's responses will be
|
||||
</p>
|
||||
<p className="text-xs text-amber-600 font-medium mt-1">
|
||||
|
|
@ -2331,7 +2331,7 @@ Controls how thoroughly GPT-5.4 thinks and how detailed responses are
|
|||
</>
|
||||
)}
|
||||
|
||||
<div className="text-xs text-slate-600">
|
||||
<div className="text-xs text-muted-foreground">
|
||||
<p><strong>GPT-5.4:</strong> Recommended model. Best quality for complex analysis and persona responses.</p>
|
||||
<p><strong>GPT-5.4 Mini:</strong> Faster and lower cost. Great for most tasks with good quality.</p>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue