diff --git a/backend/igny8_core/ai/ai_core.py b/backend/igny8_core/ai/ai_core.py index c81c9990..96ecacc3 100644 --- a/backend/igny8_core/ai/ai_core.py +++ b/backend/igny8_core/ai/ai_core.py @@ -1026,11 +1026,19 @@ class AICore: del inference_task['height'] inference_task['resolution'] = '1k' # Use 1K tier for optimal speed/quality print(f"[AI][{function_name}] Using Nano Banana config: resolution=1k (no width/height)") - else: + elif runware_model.startswith('bytedance:'): + # Seedream 4.5 (bytedance:seedream@4.5) - High quality ByteDance model + # Uses basic format with just width/height - no steps, CFGScale, or special providerSettings needed + # Just use the base inference_task as-is + print(f"[AI][{function_name}] Using Seedream 4.5 config: basic format, width={width}, height={height}") + elif runware_model.startswith('runware:'): # Hi Dream Full (runware:97@1) - General diffusion, steps 20, CFGScale 7 inference_task['steps'] = 20 inference_task['CFGScale'] = 7 print(f"[AI][{function_name}] Using Hi Dream Full config: steps=20, CFGScale=7") + else: + # Unknown model - use basic format without extra parameters + print(f"[AI][{function_name}] Using basic format for unknown model: {runware_model}") payload = [ { diff --git a/backend/igny8_core/ai/functions/generate_images.py b/backend/igny8_core/ai/functions/generate_images.py index 514bec83..e5c67776 100644 --- a/backend/igny8_core/ai/functions/generate_images.py +++ b/backend/igny8_core/ai/functions/generate_images.py @@ -70,22 +70,37 @@ class GenerateImagesFunction(BaseAIFunction): # Get image generation settings from AISettings (with account overrides) from igny8_core.modules.system.ai_settings import AISettings from igny8_core.ai.model_registry import ModelRegistry + from igny8_core.business.billing.models import AIModelConfig # Get effective settings (AISettings + AccountSettings overrides) image_style = AISettings.get_effective_image_style(account) max_images = AISettings.get_effective_max_images(account) + quality_tier = AISettings.get_effective_quality_tier(account) - # Get default image model and provider from database - default_model = ModelRegistry.get_default_model('image') - if default_model: - model_config = ModelRegistry.get_model(default_model) - provider = model_config.provider if model_config else 'openai' - model = default_model + # Get image model based on user's selected quality tier + selected_model = None + if quality_tier: + selected_model = AIModelConfig.objects.filter( + model_type='image', + quality_tier=quality_tier, + is_active=True + ).first() + + # Fall back to default model if no tier match + if not selected_model: + default_model_name = ModelRegistry.get_default_model('image') + if default_model_name: + selected_model = ModelRegistry.get_model(default_model_name) + + # Set provider and model from selected model + if selected_model: + provider = selected_model.provider if selected_model.provider else 'openai' + model = selected_model.model_name else: provider = 'openai' model = 'dall-e-3' - logger.info(f"Using image settings: provider={provider}, model={model}, style={image_style}, max={max_images}") + logger.info(f"Using image settings: provider={provider}, model={model}, tier={quality_tier}, style={image_style}, max={max_images}") return { 'tasks': tasks, diff --git a/backend/igny8_core/business/billing/models.py b/backend/igny8_core/business/billing/models.py index 94b3c453..d6544860 100644 --- a/backend/igny8_core/business/billing/models.py +++ b/backend/igny8_core/business/billing/models.py @@ -736,6 +736,7 @@ class AIModelConfig(models.Model): QUALITY_TIER_CHOICES = [ ('basic', 'Basic'), ('quality', 'Quality'), + ('quality_option2', 'Quality-Option2'), ('premium', 'Premium'), ] diff --git a/backend/igny8_core/modules/billing/migrations/0030_add_aimodel_image_sizes.py b/backend/igny8_core/modules/billing/migrations/0030_add_aimodel_image_sizes.py index d3ff03a6..6515b762 100644 --- a/backend/igny8_core/modules/billing/migrations/0030_add_aimodel_image_sizes.py +++ b/backend/igny8_core/modules/billing/migrations/0030_add_aimodel_image_sizes.py @@ -10,6 +10,42 @@ class Migration(migrations.Migration): ] operations = [ - # Fields already added via direct SQL, just mark as noop - # This ensures the model matches the database schema + # Declare the fields in Django schema so migrations can use them + # These fields already exist in DB, so state_operations syncs Django's understanding + migrations.SeparateDatabaseAndState( + state_operations=[ + migrations.AddField( + model_name='aimodelconfig', + name='landscape_size', + field=models.CharField( + blank=True, + help_text="Landscape image size for this model (e.g., '1792x1024', '1280x768')", + max_length=20, + null=True, + ), + ), + migrations.AddField( + model_name='aimodelconfig', + name='square_size', + field=models.CharField( + blank=True, + default='1024x1024', + help_text="Square image size for this model (e.g., '1024x1024')", + max_length=20, + ), + ), + migrations.AddField( + model_name='aimodelconfig', + name='valid_sizes', + field=models.JSONField( + blank=True, + default=list, + help_text="List of valid sizes for this model (e.g., ['1024x1024', '1792x1024'])", + ), + ), + ], + database_operations=[ + # No DB operations - fields already exist via direct SQL + ], + ), ] diff --git a/backend/igny8_core/modules/billing/migrations/0031_add_seedream_model.py b/backend/igny8_core/modules/billing/migrations/0031_add_seedream_model.py new file mode 100644 index 00000000..a65f913b --- /dev/null +++ b/backend/igny8_core/modules/billing/migrations/0031_add_seedream_model.py @@ -0,0 +1,80 @@ +# Generated manually - Add Seedream 4.5 image model and update quality_tier choices +from decimal import Decimal +from django.db import migrations, models + + +def add_seedream_model(apps, schema_editor): + """ + Add ByteDance Seedream 4.5 image model via Runware. + + Model specs: + - model: bytedance:seedream@4.5 + - Square size: 2048x2048 + - Landscape size: 2304x1728 + - Quality tier: quality_option2 + - Credits per image: 5 + """ + AIModelConfig = apps.get_model('billing', 'AIModelConfig') + + AIModelConfig.objects.update_or_create( + model_name='bytedance:seedream@4.5', + defaults={ + 'display_name': 'Seedream 4.5 - High Quality', + 'model_type': 'image', + 'provider': 'runware', + 'is_default': False, + 'is_active': True, + 'credits_per_image': 5, + 'quality_tier': 'quality_option2', + 'landscape_size': '2304x1728', + 'square_size': '2048x2048', + 'valid_sizes': ['2048x2048', '2304x1728', '2560x1440', '1728x2304', '1440x2560'], + 'capabilities': { + 'max_sequential_images': 4, + 'high_resolution': True, + 'provider_settings': { + 'bytedance': { + 'maxSequentialImages': 4 + } + } + }, + } + ) + print("✅ Added Seedream 4.5 image model") + + +def reverse_migration(apps, schema_editor): + """Remove Seedream model""" + AIModelConfig = apps.get_model('billing', 'AIModelConfig') + AIModelConfig.objects.filter(model_name='bytedance:seedream@4.5').delete() + print("❌ Removed Seedream 4.5 image model") + + +class Migration(migrations.Migration): + """Add Seedream 4.5 image model and update quality_tier choices.""" + + dependencies = [ + ('billing', '0030_add_aimodel_image_sizes'), + ] + + operations = [ + # Update quality_tier field choices to include quality_option2 + migrations.AlterField( + model_name='aimodelconfig', + name='quality_tier', + field=models.CharField( + blank=True, + choices=[ + ('basic', 'Basic'), + ('quality', 'Quality'), + ('quality_option2', 'Quality-Option2'), + ('premium', 'Premium'), + ], + help_text='basic / quality / quality_option2 / premium - for image models', + max_length=20, + null=True, + ), + ), + # Add the Seedream model + migrations.RunPython(add_seedream_model, reverse_migration), + ] diff --git a/backend/igny8_core/modules/system/ai_settings.py b/backend/igny8_core/modules/system/ai_settings.py index 2483aa3b..625a5b43 100644 --- a/backend/igny8_core/modules/system/ai_settings.py +++ b/backend/igny8_core/modules/system/ai_settings.py @@ -50,6 +50,7 @@ class SystemAISettings(models.Model): QUALITY_TIER_CHOICES = [ ('basic', 'Basic'), ('quality', 'Quality'), + ('quality_option2', 'Quality-Option2'), ('premium', 'Premium'), ] @@ -191,6 +192,30 @@ class SystemAISettings(models.Model): return str(override) return cls.get_instance().image_size + @classmethod + def get_effective_quality_tier(cls, account=None) -> str: + """Get quality_tier, checking account override first (from ai_settings key)""" + if account: + # Check consolidated ai_settings first + try: + from igny8_core.modules.system.settings_models import AccountSettings + setting = AccountSettings.objects.filter( + account=account, + key='ai_settings' + ).first() + if setting and setting.value: + tier = setting.value.get('quality_tier') + if tier: + return str(tier) + except Exception as e: + logger.debug(f"Could not get quality_tier from ai_settings: {e}") + + # Fall back to individual key + override = cls._get_account_override(account, 'ai.quality_tier') + if override is not None: + return str(override) + return cls.get_instance().default_quality_tier + @staticmethod def _get_account_override(account, key: str): """Get account-specific override from AccountSettings""" diff --git a/backend/igny8_core/modules/system/settings_views.py b/backend/igny8_core/modules/system/settings_views.py index 101b9df8..e9635d4a 100644 --- a/backend/igny8_core/modules/system/settings_views.py +++ b/backend/igny8_core/modules/system/settings_views.py @@ -594,10 +594,12 @@ class ContentGenerationSettingsViewSet(viewsets.ViewSet): tier = model.quality_tier or 'basic' # Avoid duplicates if not any(t['tier'] == tier for t in quality_tiers): + # Format label: quality_option2 -> "Quality-Option2" + tier_label = tier.replace('_', '-').title() if tier else 'Basic' quality_tiers.append({ 'tier': tier, 'credits': model.credits_per_image or 1, - 'label': tier.title(), + 'label': tier_label, 'description': f"{model.display_name} quality", 'model': model.model_name, }) diff --git a/backend/igny8_core/utils/ai_processor.py b/backend/igny8_core/utils/ai_processor.py index d7e17ce6..5cd41eef 100644 --- a/backend/igny8_core/utils/ai_processor.py +++ b/backend/igny8_core/utils/ai_processor.py @@ -845,24 +845,46 @@ Make sure each prompt is detailed enough for image generation, describing the vi # Reference: image-generation.php lines 79-97 import uuid logger.info(f"[AIProcessor.generate_image] Runware API key check: has_key={bool(api_key)}, key_length={len(api_key) if api_key else 0}, key_preview={api_key[:10] + '...' + api_key[-4:] if api_key and len(api_key) > 14 else 'N/A'}") + + # Build base inference task + inference_task = { + 'taskType': 'imageInference', + 'taskUUID': str(uuid.uuid4()), + 'positivePrompt': prompt, + 'negativePrompt': negative_prompt, + 'model': runware_model, + 'width': width, + 'height': height, + 'numberResults': 1, + 'outputFormat': kwargs.get('format', 'webp') + } + + # Model-specific parameter configuration + if runware_model.startswith('bria:'): + # Bria models need steps + inference_task['steps'] = 20 + elif runware_model.startswith('google:'): + # Google models use resolution instead of width/height + del inference_task['width'] + del inference_task['height'] + inference_task['resolution'] = '1k' + elif runware_model.startswith('bytedance:'): + # Seedream models use basic format - no steps, CFGScale needed + pass + elif runware_model.startswith('runware:'): + # Hi Dream Full - needs steps and CFGScale + inference_task['steps'] = 30 + inference_task['CFGScale'] = 7.5 + else: + # Unknown model - use basic format + pass + payload = [ { 'taskType': 'authentication', 'apiKey': api_key }, - { - 'taskType': 'imageInference', - 'taskUUID': str(uuid.uuid4()), - 'positivePrompt': prompt, - 'negativePrompt': negative_prompt, - 'model': runware_model, - 'width': width, - 'height': height, - 'steps': 30, - 'CFGScale': 7.5, - 'numberResults': 1, - 'outputFormat': kwargs.get('format', 'webp') - } + inference_task ] logger.info(f"[AIProcessor.generate_image] Runware request payload: model={runware_model}, width={width}, height={height}, prompt_length={len(prompt)}") diff --git a/docs/plans/COMPREHENSIVE-SYSTEM-FIX-PLAN-JAN-10-2026.md b/docs/plans/COMPREHENSIVE-SYSTEM-FIX-PLAN-JAN-10-2026.md new file mode 100644 index 00000000..629cca4a --- /dev/null +++ b/docs/plans/COMPREHENSIVE-SYSTEM-FIX-PLAN-JAN-10-2026.md @@ -0,0 +1,1016 @@ +# COMPREHENSIVE SYSTEM FIX PLAN +**Date:** January 10, 2026 +**Priority:** CRITICAL +**Status:** Analysis Complete - Ready for Implementation + +--- + +## EXECUTIVE SUMMARY + +After comprehensive system analysis, I've identified **7 critical issues** with clear root causes and detailed fixes. These issues fall into **3 categories**: + +1. **Backend Data Model Inconsistencies** (2 issues) +2. **Missing Credit Tracking & Logging** (1 major issue) +3. **Frontend Issues** (4 issues) + +**Impact:** These fixes will ensure: +- ✅ All AI functions log consistently to AI tasks, notifications, and usage logs +- ✅ Image generation properly deducts and logs credits with cost calculations +- ✅ No attribute errors in AI model configuration +- ✅ Consistent data display across all pages +- ✅ Improved UX with proper button styling and working features + +--- + +## ISSUE 1: AIModelConfig AttributeError - `input_cost_per_1m` + +### 🔴 CRITICAL - System Breaking + +**Error Message:** +``` +Failed to cluster keywords: Unexpected error: 'AIModelConfig' object has no attribute 'input_cost_per_1m' +``` + +**Root Cause:** +The `AIModelConfig` model uses field names `cost_per_1k_input` and `cost_per_1k_output`, but `model_registry.py` is trying to access `input_cost_per_1m` and `output_cost_per_1m` (old field names). + +**Location:** +- File: `/backend/igny8_core/ai/model_registry.py` line 121 +- File: `/backend/igny8_core/modules/billing/serializers.py` line 290 + +**Current Code (WRONG):** +```python +# model_registry.py line 121 +if rate_type == 'input': + return model.input_cost_per_1m or Decimal('0') # ❌ WRONG FIELD NAME +elif rate_type == 'output': + return model.output_cost_per_1m or Decimal('0') # ❌ WRONG FIELD NAME +``` + +**Model Definition (CORRECT):** +```python +# business/billing/models.py line 785-797 +cost_per_1k_input = models.DecimalField(...) # ✅ ACTUAL FIELD NAME +cost_per_1k_output = models.DecimalField(...) # ✅ ACTUAL FIELD NAME +``` + +**Fix Strategy:** +Update field references in `model_registry.py` and `serializers.py` to match actual model field names. + +**Files to Change:** +1. `backend/igny8_core/ai/model_registry.py` (1 fix) +2. `backend/igny8_core/modules/billing/serializers.py` (1 fix) + +**Impact:** +- Fixes: Clustering errors, all AI function cost calculations +- Affects: All AI operations that use ModelRegistry for cost calculation + +--- + +## ISSUE 2: Image Generation - Missing Credit Tracking & Logging + +### 🔴 CRITICAL - Business Logic Gap + +**Problem:** +Image generation does NOT: +- ❌ Log to AI tasks table (AITaskLog) +- ❌ Log to notifications +- ❌ Log to usage logs with cost calculations +- ❌ Deduct credits properly based on model configuration + +All other AI functions (clustering, content generation, idea generation) properly log to all 3 locations, but image generation is missing. + +**Root Cause Analysis:** + +**Current Image Generation Flow:** +``` +generate_images() + → ai_core.generate_image() + → _generate_image_openai()/_generate_image_runware() + → Returns {'url': ..., 'cost': ...} + → ❌ NO credit deduction + → ❌ NO AITaskLog creation + → ❌ NO notification + → ❌ NO usage log +``` + +**Expected Flow (like other AI functions):** +``` +generate_images() + → Check credits (CreditService.check_credits) + → ai_core.generate_image() + → Returns result + → Deduct credits (CreditService.deduct_credits_for_image) + → Create AITaskLog + → Create notification + → Create usage log with cost +``` + +**What Exists (Ready to Use):** +- ✅ `CreditService.calculate_credits_for_image()` - calculates credits from model config +- ✅ `CreditService.deduct_credits_for_image()` - deducts credits and creates logs +- ✅ `AIModelConfig.credits_per_image` - configured for all image models +- ✅ Notification templates for image generation + +**What's Missing:** +- ❌ Integration of credit tracking into image generation flow +- ❌ AITaskLog creation for image generation +- ❌ Notification creation for image generation +- ❌ Usage log creation with cost calculation + +**Fix Strategy:** + +### Phase 1: Integrate Credit Tracking into Image Generation + +**Step 1.1: Update `generate_images_core()` function** + +File: `backend/igny8_core/ai/functions/generate_images.py` + +Current logic (lines 203-278): +```python +def generate_images_core(task_ids, account_id, progress_callback): + # ... gets tasks ... + # ... generates images ... + # ❌ NO credit tracking + return {'success': True, 'images_created': count} +``` + +**NEW Implementation:** +```python +def generate_images_core(task_ids, account_id, progress_callback): + """Core image generation with full credit tracking""" + from igny8_core.business.billing.services.credit_service import CreditService + from igny8_core.business.notifications.services import NotificationService + from igny8_core.ai.models import AITaskLog + + # Get account + account = Account.objects.get(id=account_id) + + # Validate + fn = GenerateImagesFunction() + validated = fn.validate({'ids': task_ids}, account) + if not validated['valid']: + return {'success': False, 'error': validated['error']} + + # Prepare + data = fn.prepare({'ids': task_ids}, account) + tasks = data['tasks'] + model = data['model'] # e.g., 'dall-e-3' + + # Get model config for credits + from igny8_core.business.billing.models import AIModelConfig + model_config = AIModelConfig.objects.get(model_name=model, is_active=True) + + # Calculate total images to generate + total_images = 0 + for task in tasks: + if task.content: + total_images += 1 # Featured image + total_images += data.get('max_in_article_images', 0) # In-article images + + # Calculate total credits needed + total_credits = model_config.credits_per_image * total_images + + # CHECK CREDITS FIRST (before any generation) + if account.credits < total_credits: + error_msg = f"Insufficient credits. Required: {total_credits}, Available: {account.credits}" + # Create failed notification + NotificationService.create_notification( + account=account, + notification_type='ai_image_failed', + message=error_msg, + related_object_type='task', + related_object_id=tasks[0].id if tasks else None + ) + return {'success': False, 'error': error_msg} + + # Create AITaskLog for tracking + task_log = AITaskLog.objects.create( + account=account, + function_name='generate_images', + phase='INIT', + status='pending', + payload={'task_ids': task_ids, 'model': model} + ) + + ai_core = AICore(account=account) + images_created = 0 + total_cost_usd = 0.0 + + try: + # Process each task + for task in tasks: + if not task.content: + continue + + # Extract prompts + prompts_data = fn.build_prompt({'task': task, **data}, account) + + # Generate featured image + featured_result = ai_core.generate_image( + prompt=formatted_featured_prompt, + provider=data['provider'], + model=model, + function_name='generate_images' + ) + + if featured_result.get('url'): + # Save image + fn.save_output( + {'url': featured_result['url'], 'image_type': 'featured'}, + {'task': task, **data}, + account + ) + images_created += 1 + total_cost_usd += float(featured_result.get('cost', 0)) + + # Generate in-article images (if configured) + # ... similar logic ... + + # DEDUCT CREDITS (with usage log and cost) + from igny8_core.business.billing.services.credit_service import CreditService + from igny8_core.business.billing.models import BillingConfiguration + + # Calculate actual credits used (based on images generated) + credits_used = images_created * model_config.credits_per_image + + # Calculate cost per credit for usage log + billing_config = BillingConfiguration.get_instance() + cost_per_credit = billing_config.default_credit_price_usd + total_cost_for_log = float(credits_used) * float(cost_per_credit) + + # Deduct credits (creates CreditTransaction, CreditUsageLog) + CreditService.deduct_credits_for_image( + account=account, + model_name=model, + num_images=images_created, + description=f"Generated {images_created} images for {len(tasks)} tasks", + metadata={ + 'task_ids': task_ids, + 'images_created': images_created, + 'model': model + }, + cost_usd=total_cost_usd, # Actual AI provider cost + related_object_type='task', + related_object_id=tasks[0].id if tasks else None + ) + + # Update AITaskLog + task_log.status = 'success' + task_log.phase = 'DONE' + task_log.cost = total_cost_usd + task_log.result = { + 'images_created': images_created, + 'credits_used': credits_used, + 'tasks_processed': len(tasks) + } + task_log.save() + + # Create success notification + NotificationService.create_notification( + account=account, + notification_type='ai_image_success', + message=f'Generated {images_created} images using {credits_used} credits', + metadata={ + 'images_created': images_created, + 'credits_used': credits_used, + 'tasks_processed': len(tasks) + }, + related_object_type='task', + related_object_id=tasks[0].id if tasks else None + ) + + return { + 'success': True, + 'images_created': images_created, + 'credits_used': credits_used, + 'cost_usd': total_cost_usd, + 'message': f'Generated {images_created} images' + } + + except Exception as e: + # Update task log with error + task_log.status = 'error' + task_log.error = str(e) + task_log.save() + + # Create failed notification + NotificationService.create_notification( + account=account, + notification_type='ai_image_failed', + message=f'Image generation failed: {str(e)}', + error=str(e), + related_object_type='task', + related_object_id=tasks[0].id if tasks else None + ) + + return {'success': False, 'error': str(e)} +``` + +**Step 1.2: Ensure Notification Types Exist** + +File: `backend/igny8_core/business/notifications/services.py` + +Check if these notification types are defined: +- `ai_image_success` +- `ai_image_failed` + +If not, add them to the notification type choices. + +### Phase 2: Test All Image Generation Paths + +**Test Cases:** +1. ✅ Manual image generation via Writer module +2. ✅ Automation image generation +3. ✅ Bulk image generation +4. ✅ Insufficient credits handling +5. ✅ AI provider errors handling + +**Validation Checks:** +- [ ] AITaskLog created for each image generation run +- [ ] Credits deducted correctly based on model config +- [ ] CreditUsageLog created with correct operation_type='image_generation' +- [ ] Cost calculated correctly (provider cost + credit cost) +- [ ] Notifications created for success/failure +- [ ] Frontend credits counter updates in real-time + +--- + +## ISSUE 3: Pause/Cancel Button Colors in Automation + +### 🟡 MEDIUM - UX Issue + +**Problem:** +Pause/Cancel buttons in automation in-progress panel need better button colors for clarity. + +**Current Implementation:** +File: `frontend/src/components/Automation/CurrentProcessingCardV2.tsx` lines 268-294 + +```tsx +{currentRun.status === 'running' ? ( + } + > + {isPausing ? 'Pausing...' : 'Pause'} + +) : currentRun.status === 'paused' ? ( + } + > + {isResuming ? 'Resuming...' : 'Resume'} + +)} + +``` + +**Recommended Fix:** + +```tsx +{currentRun.status === 'running' ? ( + } + > + {isPausing ? 'Pausing...' : 'Pause'} + +) : currentRun.status === 'paused' ? ( + } + > + {isResuming ? 'Resuming...' : 'Resume'} + +)} + +``` + +**Rationale:** +- Pause: Solid warning (yellow) button - more visible, important action +- Resume: Already solid success (green) - GOOD +- Cancel: Solid danger (red) button - critical destructive action needs prominence + +**Files to Change:** +1. `frontend/src/components/Automation/CurrentProcessingCardV2.tsx` +2. `frontend/src/components/Automation/CurrentProcessingCard.tsx` (if still used) + +--- + +## ISSUE 4: Credits Not Updating in Automation In-Progress Panel + +### 🔴 CRITICAL - Real-time UX Issue + +**Problem:** +When images are being generated one by one in automation, the credits count doesn't update in the in-progress panel. + +**Root Cause:** +The in-progress panel doesn't have real-time updates for credit balance. It only updates when the page refreshes or when the run status is polled. + +**Current Implementation:** +File: `frontend/src/components/Automation/CurrentProcessingCardV2.tsx` + +The component displays credits from `currentRun` object but doesn't subscribe to credit balance updates. + +**Fix Strategy:** + +### Option 1: Poll Credit Balance (Simpler) + +Add credit balance polling to the automation progress polling: + +```tsx +// In CurrentProcessingCardV2.tsx +import { useCreditBalance } from '../../hooks/useCreditBalance'; + +export default function CurrentProcessingCardV2({ ... }) { + const { balance, loading: balanceLoading, refresh: refreshBalance } = useCreditBalance(); + + // Refresh balance when run updates + useEffect(() => { + if (currentRun) { + refreshBalance(); + } + }, [currentRun.credits_used, currentRun.credits_remaining]); + + // Display live balance + return ( +
+ The page you're looking for doesn't exist or has been moved. +
+ + {/* Actions */} ++ Need help? Contact Support +
+