Files
OpenCode 68a9071cf1 feat: reconcile local master with origin (exa/hot-platform hunters, leaks ledger, latest results)
Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
2026-08-07 12:53:47 +08:00

1 line
347 KiB
JSON

{"repo": "bensalemboualem/rag-dz", "files": {"infrastructure/docker/docker-compose.yml": {"7d6e298b582d583185cd24b914c17d9daeaa9491": {"t": 1785745256.4343567, "n": 14597, "h": "cbb03ef4ecd9d483", "c": "# ==============================================\n# IA FACTORY ALGÉRIE (RAG-DZ) - Docker Compose\n# ==============================================\n# Instance Souveraine - Région: DZ (Algérie)\n# Ports: 8180-8191 (plage réservée IAF-DZ)\n# ==============================================\n\nservices:\n # ==============================================\n # DATABASES & CACHE\n # ==============================================\n\n # PostgreSQL avec PGVector (Recherche Vectorielle)\n iafactory-postgres:\n image: pgvector/pgvector:pg16\n container_name: iaf-dz-postgres\n environment:\n POSTGRES_USER: ${POSTGRES_USER:?POSTGRES_USER requis dans .env}\n POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD requis dans .env}\n POSTGRES_DB: ${POSTGRES_DB:-archon}\n TZ: Africa/Algiers\n volumes:\n - postgres_data:/var/lib/postgresql/data\n - ./infrastructure/sql/init.sql:/docker-entrypoint-initdb.d/01-init.sql\n - ./infrastructure/sql/pgvector_migration.sql:/docker-entrypoint-initdb.d/02-pgvector.sql\n ports:\n - \"127.0.0.1:6330:5432\"\n healthcheck:\n test: [\"CMD-SHELL\", \"pg_isready -U postgres\"]\n interval: 10s\n timeout: 5s\n retries: 5\n networks:\n - iafactory-net\n command: postgres -c shared_preload_libraries=vector\n restart: unless-stopped\n\n # Redis Cache\n iafactory-redis:\n image: redis:7-alpine\n container_name: iaf-dz-redis\n command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru\n volumes:\n - redis_data:/data\n ports:\n - \"6331:6379\"\n healthcheck:\n test: [\"CMD\", \"redis-cli\", \"ping\"]\n interval: 10s\n timeout: 3s\n retries: 5\n networks:\n - iafactory-net\n restart: unless-stopped\n\n # Qdrant Vector Database\n iafactory-qdrant:\n image: qdrant/qdrant:latest\n container_name: iaf-dz-qdrant\n volumes:\n - qdrant_data:/qdrant/storage\n ports:\n - \"6332:6333\"\n environment:\n QDRANT_LOG_LEVEL: ${LOG_LEVEL:-INFO}\n networks:\n - iafactory-net\n restart: unless-stopped\n\n # ==============================================\n # BACKEND API (RAG Souverain)\n # ==============================================\n\n iafactory-backend:\n build:\n context: ./backend/rag-compat\n dockerfile: Dockerfile\n container_name: iaf-dz-backend\n env_file:\n - .env.local\n depends_on:\n iafactory-postgres:\n condition: service_healthy\n iafactory-redis:\n condition: service_healthy\n iafactory-qdrant:\n condition: service_started\n environment:\n # Région Souveraine\n SOVEREIGNTY_REGION: DZ\n SOVEREIGNTY_LABEL: \"Algérie\"\n TZ: Africa/Algiers\n\n # Database - uses env vars for password\n POSTGRES_URL: postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD}@iafactory-postgres:5432/${POSTGRES_DB:-archon}\n DATABASE_URL: postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD}@iafactory-postgres:5432/${POSTGRES_DB:-archon}\n\n # Redis\n REDIS_URL: redis://iafactory-redis:6379/0\n REDIS_PASSWORD: ${REDIS_PASSWORD:-}\n\n # Qdrant\n QDRANT_HOST: iafactory-qdrant\n QDRANT_PORT: 6333\n\n # LLM Provider (Groq = gratuit et rapide)\n ENABLE_LLM: \"true\"\n LLM_PROVIDER: ${LLM_PROVIDER:-groq}\n LLM_MODEL: ${LLM_MODEL:-llama-3.3-70b-versatile}\n GROQ_API_KEY: ${GROQ_API_KEY:-}\n OPENAI_API_KEY: ${OPENAI_API_KEY:-}\n ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-}\n DEEPSEEK_API_KEY: ${DEEPSEEK_API_KEY:-}\n GOOGLE_GENERATIVE_AI_API_KEY: ${GOOGLE_GENERATIVE_AI_API_KEY:-}\n\n # TTS Voice (ElevenLabs pour arabe haute qualité)\n ELEVENLABS_API_KEY: ${ELEVENLABS_API_KEY:-}\n\n # Security - REQUIRED: set in .env file\n API_SECRET_KEY: ${API_SECRET_KEY:?API_SECRET_KEY requis dans .env}\n JWT_SECRET_KEY: ${JWT_SECRET_KEY:?JWT_SECRET_KEY requis dans .env}\n ALLOWED_ORIGINS: http://localhost:8180,http://localhost:8181,http://localhost:8182,http://localhost:8183,http://localhost:8184,http://localhost:8185\n\n # Rate Limiting\n RATE_LIMIT_PER_MINUTE: ${RATE_LIMIT_PER_MINUTE:-60}\n RATE_LIMIT_PER_HOUR: ${RATE_LIMIT_PER_HOUR:-1000}\n ENABLE_RATE_LIMITING: ${ENABLE_RATE_LIMITING:-true}\n\n # Embeddings (Multilingue FR/AR/EN)\n EMBEDDING_MODEL: ${EMBEDDING_MODEL:-sentence-transformers/paraphrase-multilingual-mpnet-base-v2}\n EMBEDDING_DEVICE: ${EMBEDDING_DEVICE:-cpu}\n EMBEDDING_BATCH_SIZE: ${EMBEDDING_BATCH_SIZE:-32}\n\n # Reranking\n USE_RERANKING: ${USE_RERANKING:-true}\n RERANKING_MODEL: ${RERANKING_MODEL:-cross-encoder/ms-marco-MiniLM-L-6-v2}\n RERANKING_TOP_K: ${RERANKING_TOP_K:-10}\n\n # PGVector\n USE_PGVECTOR: ${USE_PGVECTOR:-true}\n\n # Service Info\n SERVICE_NAME: iafactory-dz-api\n SERVICE_VERSION: 1.0.0\n LOG_LEVEL: ${LOG_LEVEL:-INFO}\n ENVIRONMENT: ${ENVIRONMENT:-development}\n\n # Features\n ENABLE_CORS: ${ENABLE_CORS:-true}\n ENABLE_METRICS: ${ENABLE_METRICS:-true}\n ENABLE_API_KEY_AUTH: ${ENABLE_API_KEY_AUTH:-false}\n\n # BMAD Configuration\n BMAD_DEFAULT_PROVIDER: ${BMAD_DEFAULT_PROVIDER:-groq}\n\n # Twilio SMS & WhatsApp (DZ)\n TWILIO_ACCOUNT_SID: ${TWILIO_ACCOUNT_SID:-}\n TWILIO_AUTH_TOKEN: ${TWILIO_AUTH_TOKEN:-}\n TWILIO_PHONE_NUMBER: ${TWILIO_PHONE_NUMBER:-}\n TWILIO_WHATSAPP_NUMBER: ${TWILIO_WHATSAPP_NUMBER:-}\n\n ports:\n - \"8180:8180\"\n volumes:\n - ./backend/rag-compat:/app\n - ./bmad:/bmad:ro\n - backend_cache:/root/.cache\n healthcheck:\n test: [\"CMD\", \"curl\", \"-f\", \"http://localhost:8180/health\"]\n interval: 30s\n timeout: 10s\n retries: 3\n start_period: 40s\n networks:\n - iafactory-net\n restart: unless-stopped\n\n # ==============================================\n # FRONTEND APPLICATIONS\n # ==============================================\n\n # IAFactory Hub (Dashboard Principal)\n iafactory-hub:\n build:\n context: ./frontend/archon-ui\n dockerfile: Dockerfile\n args:\n VITE_API_URL: ${VITE_API_URL:-http://localhost:8180}\n VITE_API_SECRET_KEY: ${API_SECRET_KEY}\n container_name: iaf-dz-hub\n depends_on:\n - iafactory-backend\n environment:\n VITE_API_URL: ${VITE_API_URL:-http://localhost:8180}\n VITE_WS_URL: ${VITE_WS_URL:-ws://localhost:8180/ws}\n VITE_SHOW_DEVTOOLS: ${VITE_SHOW_DEVTOOLS:-true}\n VITE_API_SECRET_KEY: ${API_SECRET_KEY}\n VITE_API_KEY: ${API_SECRET_KEY}\n VITE_SOVEREIGNTY_REGION: DZ\n ports:\n - \"8182:3737\"\n volumes:\n - ./frontend/archon-ui/src:/app/src\n - ./frontend/archon-ui/public:/app/public\n networks:\n - iafactory-net\n restart: unless-stopped\n\n # IAFactory Docs (Gestion Documentaire)\n iafactory-docs:\n build:\n context: ./frontend/rag-ui\n dockerfile: Dockerfile\n args:\n VITE_API_URL: ${VITE_API_URL:-http://localhost:8180}\n container_name: iaf-dz-docs\n depends_on:\n - iafactory-backend\n environment:\n REACT_APP_API_URL: ${VITE_API_URL:-http://localhost:8180}\n VITE_SOVEREIGNTY_REGION: DZ\n ports:\n - \"8183:5173\"\n volumes:\n - ./frontend/rag-ui/src:/app/src\n - ./frontend/rag-ui/public:/app/public\n networks:\n - iafactory-net\n restart: unless-stopped\n\n # IAFactory Studio (Bolt.DIY - AI Code Editor)\n iafactory-studio:\n build:\n context: ./bolt-diy\n dockerfile: Dockerfile\n container_name: iaf-dz-studio\n depends_on:\n - iafactory-backend\n environment:\n VITE_API_URL: ${VITE_API_URL:-http://localhost:8180}\n VITE_ARCHON_API_URL: http://localhost:8180\n OPENAI_API_KEY: ${OPENAI_API_KEY:-}\n ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-}\n GROQ_API_KEY: ${GROQ_API_KEY:-}\n DEEPSEEK_API_KEY: ${DEEPSEEK_API_KEY:-}\n GOOGLE_GENERATIVE_AI_API_KEY: ${GOOGLE_GENERATIVE_AI_API_KEY:-}\n OLLAMA_API_BASE_URL: http://iafactory-ollama:11434\n VITE_SOVEREIGNTY_REGION: DZ\n volumes:\n - ./bolt-diy/app:/app/app:ro\n - ./bolt-diy/public:/app/public\n ports:\n - \"8184:5173\"\n networks:\n - iafactory-net\n restart: unless-stopped\n profiles:\n - studio\n\n # ==============================================\n # SEO & LANDING PAGES\n # ==============================================\n\n # SEO-DZ-Boost - Landing Pages SEO pour Google Algérie\n iafactory-seo-dz:\n build:\n context: ./apps/seo-dz-boost\n dockerfile: Dockerfile\n container_name: iaf-dz-seo\n environment:\n ANALYTICS_PROVIDER: ${ANALYTICS_PROVIDER:-plausible}\n ANALYTICS_ID: ${ANALYTICS_ID:-}\n GA4_MEASUREMENT_ID: ${GA4_MEASUREMENT_ID:-}\n PLAUSIBLE_DOMAIN: ${PLAUSIBLE_DOMAIN:-www.iafactoryalgeria.com}\n MATOMO_URL: ${MATOMO_URL:-}\n MATOMO_SITE_ID: ${MATOMO_SITE_ID:-}\n ports:\n - \"8218:80\"\n networks:\n - iafactory-net\n restart: unless-stopped\n healthcheck:\n test: [\"CMD\", \"curl\", \"-f\", \"http://localhost:80/\"]\n interval: 30s\n timeout: 10s\n retries: 3\n labels:\n - \"traefik.enable=true\"\n - \"traefik.http.routers.seo-dz.rule=Host(`www.iafactoryalgeria.com`)\"\n - \"traefik.http.services.seo-dz.loadbalancer.server.port=80\"\n\n # ==============================================\n # DEVELOPER PORTAL (Module 16)\n # ==============================================\n\n # API Portal - Dashboard développeur façon OpenAI/Stripe\n iafactory-api-portal:\n build:\n context: ./apps/api-portal/frontend\n dockerfile: Dockerfile\n container_name: iaf-dz-api-portal\n depends_on:\n - iafactory-backend\n environment:\n VITE_API_URL: ${VITE_API_URL:-http://localhost:8180}\n VITE_SOVEREIGNTY_REGION: DZ\n ports:\n - \"8219:3000\"\n networks:\n - iafactory-net\n restart: unless-stopped\n healthcheck:\n test: [\"CMD\", \"curl\", \"-f\", \"http://localhost:3000/\"]\n interval: 30s\n timeout: 10s\n retries: 3\n labels:\n - \"traefik.enable=true\"\n - \"traefik.http.routers.api-portal.rule=Host(`dev.iafactoryalgeria.com`)\"\n - \"traefik.http.services.api-portal.loadbalancer.server.port=3000\"\n\n # ==============================================\n # WORKFLOW & AUTOMATION\n # ==============================================\n\n # n8n Workflow Automation\n iafactory-n8n:\n image: docker.n8n.io/n8nio/n8n:latest\n container_name: iaf-dz-n8n\n depends_on:\n iafactory-postgres:\n condition: service_healthy\n environment:\n # Database\n DB_TYPE: postgresdb\n DB_POSTGRESDB_HOST: iafactory-postgres\n DB_POSTGRESDB_PORT: 5432\n DB_POSTGRESDB_DATABASE: ${POSTGRES_DB:-archon}\n DB_POSTGRESDB_USER: ${POSTGRES_USER:-postgres}\n DB_POSTGRESDB_PASSWORD: ${POSTGRES_PASSWORD}\n DB_POSTGRESDB_SCHEMA: n8n\n\n # Auth\n N8N_BASIC_AUTH_ACTIVE: ${N8N_BASIC_AUTH_ACTIVE:-true}\n N8N_BASIC_AUTH_USER: ${N8N_BASIC_AUTH_USER:-admin}\n N8N_BASIC_AUTH_PASSWORD: ${N8N_BASIC_AUTH_PASSWORD:-admin}\n\n # Webhook\n WEBHOOK_URL: ${N8N_WEBHOOK_URL:-http://localhost:8185}\n N8N_HOST: ${N8N_HOST:-localhost}\n N8N_PORT: 5678\n N8N_PROTOCOL: http\n\n # Timezone Algérie\n GENERIC_TIMEZONE: Africa/Algiers\n TZ: Africa/Algiers\n\n # Execution\n EXECUTIONS_DATA_PRUNE: \"true\"\n EXECUTIONS_DATA_MAX_AGE: 168\n\n # API\n N8N_API_ENABLED: \"true\"\n N8N_DIAGNOSTICS_ENABLED: \"false\"\n N8N_PERSONALIZATION_ENABLED: \"false\"\n volumes:\n - n8n_data:/home/node/.n8n\n - ./infrastructure/n8n/workflows:/home/node/workflows:ro\n ports:\n - \"8185:5678\"\n networks:\n - iafactory-net\n restart: unless-stopped\n\n # ==============================================\n # AI MODELS (Optionnel)\n # ==============================================\n\n # Ollama - Modèles IA Locaux (pour LLM Council)\n iafactory-ollama:\n image: ollama/ollama:latest\n container_name: iaf-dz-ollama\n volumes:\n - ollama_data:/root/.ollama\n ports:\n - \"127.0.0.1:11434:11434\"\n networks:\n - iafactory-net\n restart: unless-stopped\n healthcheck:\n test: [\"CMD\", \"curl\", \"-f\", \"http://localhost:11434/api/tags\"]\n interval: 30s\n timeout: 10s\n retries: 3\n # Pour GPU support (optionnel):\n # deploy:\n # resources:\n # reservations:\n # devices:\n # - driver: nvidia\n # count: 1\n # capabilities: [gpu]\n\n # ==============================================\n # MONITORING (Optionnel)\n # ==============================================\n\n # Prometheus\n iafactory-prometheus:\n image: prom/prometheus:latest\n container_name: iaf-dz-prometheus\n volumes:\n - ./infrastructure/monitoring/prometheus.yml:/etc/prometheus/prometheus.yml\n - ./infrastructure/monitoring/alerts.yml:/etc/prometheus/alerts.yml\n - prometheus_data:/prometheus\n command:\n - '--config.file=/etc/prometheus/prometheus.yml'\n - '--storage.tsdb.path=/prometheus'\n - '--storage.tsdb.retention.time=30d'\n ports:\n - \"8187:9090\"\n networks:\n - iafactory-net\n restart: unless-stopped\n profiles:\n - monitoring\n\n # Grafana\n iafactory-grafana:\n image: grafana/grafana:latest\n container_name: iaf-dz-grafana\n depends_on:\n - iafactory-prometheus\n environment:\n GF_SECURITY_ADMIN_USER: ${GRAFANA_USER:-admin}\n GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_PASSWORD:-admin}\n GF_USERS_ALLOW_SIGN_UP: \"false\"\n GF_SERVER_ROOT_URL: http://localhost:8188\n GF_INSTALL_PLUGINS: redis-datasource\n volumes:\n - ./infrastructure/monitoring/grafana/dashboards:/etc/grafana/provisioning/dashboards\n - ./infrastructure/monitoring/grafana/datasources:/etc/grafana/provisioning/datasources\n - grafana_data:/var/lib/grafana\n ports:\n - \"8188:3000\"\n networks:\n - iafactory-net\n restart: unless-stopped\n profiles:\n - monitoring\n\n# ==============================================\n# NETWORKS\n# ==============================================\nnetworks:\n iafactory-net:\n driver: bridge\n name: iafactory-net\n\n# ==============================================\n# VOLUMES\n# ==============================================\nvolumes:\n postgres_data:\n name: iaf-dz-postgres-data\n redis_data:\n name: iaf-dz-redis-data\n qdrant_data:\n name: iaf-dz-qdrant-data\n ollama_data:\n name: iaf-dz-ollama-data\n backend_cache:\n name: iaf-dz-backend-cache\n prometheus_data:\n name: iaf-dz-prometheus-data\n grafana_data:\n name: iaf-dz-grafana-data\n n8n_data:\n name: iaf-dz-n8n-data\n"}, "cec8535cc766b8b211c8bee394ecca3c44b713a7": {"t": 1785749289.4335523, "n": 14597, "h": "cbb03ef4ecd9d483", "c": "# ==============================================\n# IA FACTORY ALGÉRIE (RAG-DZ) - Docker Compose\n# ==============================================\n# Instance Souveraine - Région: DZ (Algérie)\n# Ports: 8180-8191 (plage réservée IAF-DZ)\n# ==============================================\n\nservices:\n # ==============================================\n # DATABASES & CACHE\n # ==============================================\n\n # PostgreSQL avec PGVector (Recherche Vectorielle)\n iafactory-postgres:\n image: pgvector/pgvector:pg16\n container_name: iaf-dz-postgres\n environment:\n POSTGRES_USER: ${POSTGRES_USER:?POSTGRES_USER requis dans .env}\n POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD requis dans .env}\n POSTGRES_DB: ${POSTGRES_DB:-archon}\n TZ: Africa/Algiers\n volumes:\n - postgres_data:/var/lib/postgresql/data\n - ./infrastructure/sql/init.sql:/docker-entrypoint-initdb.d/01-init.sql\n - ./infrastructure/sql/pgvector_migration.sql:/docker-entrypoint-initdb.d/02-pgvector.sql\n ports:\n - \"127.0.0.1:6330:5432\"\n healthcheck:\n test: [\"CMD-SHELL\", \"pg_isready -U postgres\"]\n interval: 10s\n timeout: 5s\n retries: 5\n networks:\n - iafactory-net\n command: postgres -c shared_preload_libraries=vector\n restart: unless-stopped\n\n # Redis Cache\n iafactory-redis:\n image: redis:7-alpine\n container_name: iaf-dz-redis\n command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru\n volumes:\n - redis_data:/data\n ports:\n - \"6331:6379\"\n healthcheck:\n test: [\"CMD\", \"redis-cli\", \"ping\"]\n interval: 10s\n timeout: 3s\n retries: 5\n networks:\n - iafactory-net\n restart: unless-stopped\n\n # Qdrant Vector Database\n iafactory-qdrant:\n image: qdrant/qdrant:latest\n container_name: iaf-dz-qdrant\n volumes:\n - qdrant_data:/qdrant/storage\n ports:\n - \"6332:6333\"\n environment:\n QDRANT_LOG_LEVEL: ${LOG_LEVEL:-INFO}\n networks:\n - iafactory-net\n restart: unless-stopped\n\n # ==============================================\n # BACKEND API (RAG Souverain)\n # ==============================================\n\n iafactory-backend:\n build:\n context: ./backend/rag-compat\n dockerfile: Dockerfile\n container_name: iaf-dz-backend\n env_file:\n - .env.local\n depends_on:\n iafactory-postgres:\n condition: service_healthy\n iafactory-redis:\n condition: service_healthy\n iafactory-qdrant:\n condition: service_started\n environment:\n # Région Souveraine\n SOVEREIGNTY_REGION: DZ\n SOVEREIGNTY_LABEL: \"Algérie\"\n TZ: Africa/Algiers\n\n # Database - uses env vars for password\n POSTGRES_URL: postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD}@iafactory-postgres:5432/${POSTGRES_DB:-archon}\n DATABASE_URL: postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD}@iafactory-postgres:5432/${POSTGRES_DB:-archon}\n\n # Redis\n REDIS_URL: redis://iafactory-redis:6379/0\n REDIS_PASSWORD: ${REDIS_PASSWORD:-}\n\n # Qdrant\n QDRANT_HOST: iafactory-qdrant\n QDRANT_PORT: 6333\n\n # LLM Provider (Groq = gratuit et rapide)\n ENABLE_LLM: \"true\"\n LLM_PROVIDER: ${LLM_PROVIDER:-groq}\n LLM_MODEL: ${LLM_MODEL:-llama-3.3-70b-versatile}\n GROQ_API_KEY: ${GROQ_API_KEY:-}\n OPENAI_API_KEY: ${OPENAI_API_KEY:-}\n ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-}\n DEEPSEEK_API_KEY: ${DEEPSEEK_API_KEY:-}\n GOOGLE_GENERATIVE_AI_API_KEY: ${GOOGLE_GENERATIVE_AI_API_KEY:-}\n\n # TTS Voice (ElevenLabs pour arabe haute qualité)\n ELEVENLABS_API_KEY: ${ELEVENLABS_API_KEY:-}\n\n # Security - REQUIRED: set in .env file\n API_SECRET_KEY: ${API_SECRET_KEY:?API_SECRET_KEY requis dans .env}\n JWT_SECRET_KEY: ${JWT_SECRET_KEY:?JWT_SECRET_KEY requis dans .env}\n ALLOWED_ORIGINS: http://localhost:8180,http://localhost:8181,http://localhost:8182,http://localhost:8183,http://localhost:8184,http://localhost:8185\n\n # Rate Limiting\n RATE_LIMIT_PER_MINUTE: ${RATE_LIMIT_PER_MINUTE:-60}\n RATE_LIMIT_PER_HOUR: ${RATE_LIMIT_PER_HOUR:-1000}\n ENABLE_RATE_LIMITING: ${ENABLE_RATE_LIMITING:-true}\n\n # Embeddings (Multilingue FR/AR/EN)\n EMBEDDING_MODEL: ${EMBEDDING_MODEL:-sentence-transformers/paraphrase-multilingual-mpnet-base-v2}\n EMBEDDING_DEVICE: ${EMBEDDING_DEVICE:-cpu}\n EMBEDDING_BATCH_SIZE: ${EMBEDDING_BATCH_SIZE:-32}\n\n # Reranking\n USE_RERANKING: ${USE_RERANKING:-true}\n RERANKING_MODEL: ${RERANKING_MODEL:-cross-encoder/ms-marco-MiniLM-L-6-v2}\n RERANKING_TOP_K: ${RERANKING_TOP_K:-10}\n\n # PGVector\n USE_PGVECTOR: ${USE_PGVECTOR:-true}\n\n # Service Info\n SERVICE_NAME: iafactory-dz-api\n SERVICE_VERSION: 1.0.0\n LOG_LEVEL: ${LOG_LEVEL:-INFO}\n ENVIRONMENT: ${ENVIRONMENT:-development}\n\n # Features\n ENABLE_CORS: ${ENABLE_CORS:-true}\n ENABLE_METRICS: ${ENABLE_METRICS:-true}\n ENABLE_API_KEY_AUTH: ${ENABLE_API_KEY_AUTH:-false}\n\n # BMAD Configuration\n BMAD_DEFAULT_PROVIDER: ${BMAD_DEFAULT_PROVIDER:-groq}\n\n # Twilio SMS & WhatsApp (DZ)\n TWILIO_ACCOUNT_SID: ${TWILIO_ACCOUNT_SID:-}\n TWILIO_AUTH_TOKEN: ${TWILIO_AUTH_TOKEN:-}\n TWILIO_PHONE_NUMBER: ${TWILIO_PHONE_NUMBER:-}\n TWILIO_WHATSAPP_NUMBER: ${TWILIO_WHATSAPP_NUMBER:-}\n\n ports:\n - \"8180:8180\"\n volumes:\n - ./backend/rag-compat:/app\n - ./bmad:/bmad:ro\n - backend_cache:/root/.cache\n healthcheck:\n test: [\"CMD\", \"curl\", \"-f\", \"http://localhost:8180/health\"]\n interval: 30s\n timeout: 10s\n retries: 3\n start_period: 40s\n networks:\n - iafactory-net\n restart: unless-stopped\n\n # ==============================================\n # FRONTEND APPLICATIONS\n # ==============================================\n\n # IAFactory Hub (Dashboard Principal)\n iafactory-hub:\n build:\n context: ./frontend/archon-ui\n dockerfile: Dockerfile\n args:\n VITE_API_URL: ${VITE_API_URL:-http://localhost:8180}\n VITE_API_SECRET_KEY: ${API_SECRET_KEY}\n container_name: iaf-dz-hub\n depends_on:\n - iafactory-backend\n environment:\n VITE_API_URL: ${VITE_API_URL:-http://localhost:8180}\n VITE_WS_URL: ${VITE_WS_URL:-ws://localhost:8180/ws}\n VITE_SHOW_DEVTOOLS: ${VITE_SHOW_DEVTOOLS:-true}\n VITE_API_SECRET_KEY: ${API_SECRET_KEY}\n VITE_API_KEY: ${API_SECRET_KEY}\n VITE_SOVEREIGNTY_REGION: DZ\n ports:\n - \"8182:3737\"\n volumes:\n - ./frontend/archon-ui/src:/app/src\n - ./frontend/archon-ui/public:/app/public\n networks:\n - iafactory-net\n restart: unless-stopped\n\n # IAFactory Docs (Gestion Documentaire)\n iafactory-docs:\n build:\n context: ./frontend/rag-ui\n dockerfile: Dockerfile\n args:\n VITE_API_URL: ${VITE_API_URL:-http://localhost:8180}\n container_name: iaf-dz-docs\n depends_on:\n - iafactory-backend\n environment:\n REACT_APP_API_URL: ${VITE_API_URL:-http://localhost:8180}\n VITE_SOVEREIGNTY_REGION: DZ\n ports:\n - \"8183:5173\"\n volumes:\n - ./frontend/rag-ui/src:/app/src\n - ./frontend/rag-ui/public:/app/public\n networks:\n - iafactory-net\n restart: unless-stopped\n\n # IAFactory Studio (Bolt.DIY - AI Code Editor)\n iafactory-studio:\n build:\n context: ./bolt-diy\n dockerfile: Dockerfile\n container_name: iaf-dz-studio\n depends_on:\n - iafactory-backend\n environment:\n VITE_API_URL: ${VITE_API_URL:-http://localhost:8180}\n VITE_ARCHON_API_URL: http://localhost:8180\n OPENAI_API_KEY: ${OPENAI_API_KEY:-}\n ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-}\n GROQ_API_KEY: ${GROQ_API_KEY:-}\n DEEPSEEK_API_KEY: ${DEEPSEEK_API_KEY:-}\n GOOGLE_GENERATIVE_AI_API_KEY: ${GOOGLE_GENERATIVE_AI_API_KEY:-}\n OLLAMA_API_BASE_URL: http://iafactory-ollama:11434\n VITE_SOVEREIGNTY_REGION: DZ\n volumes:\n - ./bolt-diy/app:/app/app:ro\n - ./bolt-diy/public:/app/public\n ports:\n - \"8184:5173\"\n networks:\n - iafactory-net\n restart: unless-stopped\n profiles:\n - studio\n\n # ==============================================\n # SEO & LANDING PAGES\n # ==============================================\n\n # SEO-DZ-Boost - Landing Pages SEO pour Google Algérie\n iafactory-seo-dz:\n build:\n context: ./apps/seo-dz-boost\n dockerfile: Dockerfile\n container_name: iaf-dz-seo\n environment:\n ANALYTICS_PROVIDER: ${ANALYTICS_PROVIDER:-plausible}\n ANALYTICS_ID: ${ANALYTICS_ID:-}\n GA4_MEASUREMENT_ID: ${GA4_MEASUREMENT_ID:-}\n PLAUSIBLE_DOMAIN: ${PLAUSIBLE_DOMAIN:-www.iafactoryalgeria.com}\n MATOMO_URL: ${MATOMO_URL:-}\n MATOMO_SITE_ID: ${MATOMO_SITE_ID:-}\n ports:\n - \"8218:80\"\n networks:\n - iafactory-net\n restart: unless-stopped\n healthcheck:\n test: [\"CMD\", \"curl\", \"-f\", \"http://localhost:80/\"]\n interval: 30s\n timeout: 10s\n retries: 3\n labels:\n - \"traefik.enable=true\"\n - \"traefik.http.routers.seo-dz.rule=Host(`www.iafactoryalgeria.com`)\"\n - \"traefik.http.services.seo-dz.loadbalancer.server.port=80\"\n\n # ==============================================\n # DEVELOPER PORTAL (Module 16)\n # ==============================================\n\n # API Portal - Dashboard développeur façon OpenAI/Stripe\n iafactory-api-portal:\n build:\n context: ./apps/api-portal/frontend\n dockerfile: Dockerfile\n container_name: iaf-dz-api-portal\n depends_on:\n - iafactory-backend\n environment:\n VITE_API_URL: ${VITE_API_URL:-http://localhost:8180}\n VITE_SOVEREIGNTY_REGION: DZ\n ports:\n - \"8219:3000\"\n networks:\n - iafactory-net\n restart: unless-stopped\n healthcheck:\n test: [\"CMD\", \"curl\", \"-f\", \"http://localhost:3000/\"]\n interval: 30s\n timeout: 10s\n retries: 3\n labels:\n - \"traefik.enable=true\"\n - \"traefik.http.routers.api-portal.rule=Host(`dev.iafactoryalgeria.com`)\"\n - \"traefik.http.services.api-portal.loadbalancer.server.port=3000\"\n\n # ==============================================\n # WORKFLOW & AUTOMATION\n # ==============================================\n\n # n8n Workflow Automation\n iafactory-n8n:\n image: docker.n8n.io/n8nio/n8n:latest\n container_name: iaf-dz-n8n\n depends_on:\n iafactory-postgres:\n condition: service_healthy\n environment:\n # Database\n DB_TYPE: postgresdb\n DB_POSTGRESDB_HOST: iafactory-postgres\n DB_POSTGRESDB_PORT: 5432\n DB_POSTGRESDB_DATABASE: ${POSTGRES_DB:-archon}\n DB_POSTGRESDB_USER: ${POSTGRES_USER:-postgres}\n DB_POSTGRESDB_PASSWORD: ${POSTGRES_PASSWORD}\n DB_POSTGRESDB_SCHEMA: n8n\n\n # Auth\n N8N_BASIC_AUTH_ACTIVE: ${N8N_BASIC_AUTH_ACTIVE:-true}\n N8N_BASIC_AUTH_USER: ${N8N_BASIC_AUTH_USER:-admin}\n N8N_BASIC_AUTH_PASSWORD: ${N8N_BASIC_AUTH_PASSWORD:-admin}\n\n # Webhook\n WEBHOOK_URL: ${N8N_WEBHOOK_URL:-http://localhost:8185}\n N8N_HOST: ${N8N_HOST:-localhost}\n N8N_PORT: 5678\n N8N_PROTOCOL: http\n\n # Timezone Algérie\n GENERIC_TIMEZONE: Africa/Algiers\n TZ: Africa/Algiers\n\n # Execution\n EXECUTIONS_DATA_PRUNE: \"true\"\n EXECUTIONS_DATA_MAX_AGE: 168\n\n # API\n N8N_API_ENABLED: \"true\"\n N8N_DIAGNOSTICS_ENABLED: \"false\"\n N8N_PERSONALIZATION_ENABLED: \"false\"\n volumes:\n - n8n_data:/home/node/.n8n\n - ./infrastructure/n8n/workflows:/home/node/workflows:ro\n ports:\n - \"8185:5678\"\n networks:\n - iafactory-net\n restart: unless-stopped\n\n # ==============================================\n # AI MODELS (Optionnel)\n # ==============================================\n\n # Ollama - Modèles IA Locaux (pour LLM Council)\n iafactory-ollama:\n image: ollama/ollama:latest\n container_name: iaf-dz-ollama\n volumes:\n - ollama_data:/root/.ollama\n ports:\n - \"127.0.0.1:11434:11434\"\n networks:\n - iafactory-net\n restart: unless-stopped\n healthcheck:\n test: [\"CMD\", \"curl\", \"-f\", \"http://localhost:11434/api/tags\"]\n interval: 30s\n timeout: 10s\n retries: 3\n # Pour GPU support (optionnel):\n # deploy:\n # resources:\n # reservations:\n # devices:\n # - driver: nvidia\n # count: 1\n # capabilities: [gpu]\n\n # ==============================================\n # MONITORING (Optionnel)\n # ==============================================\n\n # Prometheus\n iafactory-prometheus:\n image: prom/prometheus:latest\n container_name: iaf-dz-prometheus\n volumes:\n - ./infrastructure/monitoring/prometheus.yml:/etc/prometheus/prometheus.yml\n - ./infrastructure/monitoring/alerts.yml:/etc/prometheus/alerts.yml\n - prometheus_data:/prometheus\n command:\n - '--config.file=/etc/prometheus/prometheus.yml'\n - '--storage.tsdb.path=/prometheus'\n - '--storage.tsdb.retention.time=30d'\n ports:\n - \"8187:9090\"\n networks:\n - iafactory-net\n restart: unless-stopped\n profiles:\n - monitoring\n\n # Grafana\n iafactory-grafana:\n image: grafana/grafana:latest\n container_name: iaf-dz-grafana\n depends_on:\n - iafactory-prometheus\n environment:\n GF_SECURITY_ADMIN_USER: ${GRAFANA_USER:-admin}\n GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_PASSWORD:-admin}\n GF_USERS_ALLOW_SIGN_UP: \"false\"\n GF_SERVER_ROOT_URL: http://localhost:8188\n GF_INSTALL_PLUGINS: redis-datasource\n volumes:\n - ./infrastructure/monitoring/grafana/dashboards:/etc/grafana/provisioning/dashboards\n - ./infrastructure/monitoring/grafana/datasources:/etc/grafana/provisioning/datasources\n - grafana_data:/var/lib/grafana\n ports:\n - \"8188:3000\"\n networks:\n - iafactory-net\n restart: unless-stopped\n profiles:\n - monitoring\n\n# ==============================================\n# NETWORKS\n# ==============================================\nnetworks:\n iafactory-net:\n driver: bridge\n name: iafactory-net\n\n# ==============================================\n# VOLUMES\n# ==============================================\nvolumes:\n postgres_data:\n name: iaf-dz-postgres-data\n redis_data:\n name: iaf-dz-redis-data\n qdrant_data:\n name: iaf-dz-qdrant-data\n ollama_data:\n name: iaf-dz-ollama-data\n backend_cache:\n name: iaf-dz-backend-cache\n prometheus_data:\n name: iaf-dz-prometheus-data\n grafana_data:\n name: iaf-dz-grafana-data\n n8n_data:\n name: iaf-dz-n8n-data\n"}}, "agents/core/base_agent.py": {"7d6e298b582d583185cd24b914c17d9daeaa9491": {"t": 1785745450.5017703, "n": 3356, "h": "08ef0525238a404f", "c": "\"\"\"\nBase Agent Class for IA Factory\n\"\"\"\nfrom typing import List, Dict, Any, Optional, Callable\nfrom pydantic import BaseModel, Field\nfrom abc import ABC, abstractmethod\nfrom openai import AsyncOpenAI\nfrom anthropic import AsyncAnthropic\nimport logging\nfrom datetime import datetime\nimport os\n\nlogger = logging.getLogger(__name__)\n\nclass AgentConfig(BaseModel):\n name: str\n model: str = \"deepseek-chat\"\n temperature: float = 0.7\n max_tokens: int = 2000\n tools: List[str] = Field(default_factory=list)\n memory_enabled: bool = True\n language: str = \"fr\"\n system_prompt: Optional[str] = None\n\nclass AgentMessage(BaseModel):\n role: str\n content: str\n timestamp: datetime = Field(default_factory=datetime.now)\n metadata: Dict[str, Any] = Field(default_factory=dict)\n\nclass AgentResponse(BaseModel):\n content: str\n tools_used: List[str] = Field(default_factory=list)\n metadata: Dict[str, Any] = Field(default_factory=dict)\n tokens_used: Optional[int] = None\n duration_ms: Optional[float] = None\n\nclass BaseAgent(ABC):\n def __init__(self, config: AgentConfig):\n self.config = config\n self.memory: List[AgentMessage] = [] if config.memory_enabled else None\n self.tools_registry: Dict[str, Callable] = {}\n self.client = self._init_llm_client()\n logger.info(f\"Initialized {config.name} with model {config.model}\")\n \n def _init_llm_client(self):\n model = self.config.model.lower()\n if \"deepseek\" in model:\n return AsyncOpenAI(\n api_key=os.getenv(\"DEEPSEEK_API_KEY\"),\n base_url=\"https://api.deepseek.com\"\n )\n elif model.startswith(\"gpt\"):\n return AsyncOpenAI(api_key=os.getenv(\"OPENAI_API_KEY\"))\n elif model.startswith(\"claude\"):\n return AsyncAnthropic(api_key=os.getenv(\"ANTHROPIC_API_KEY\"))\n return AsyncOpenAI(api_key=os.getenv(\"DEEPSEEK_API_KEY\"), base_url=\"https://api.deepseek.com\")\n \n @abstractmethod\n async def execute(self, input_data: Dict[str, Any]) -> AgentResponse:\n pass\n \n def add_memory(self, message: AgentMessage):\n if self.memory is not None:\n self.memory.append(message)\n if len(self.memory) > 50:\n self.memory = self.memory[-50:]\n \n async def _call_llm(self, messages: List[Dict[str, str]]) -> Dict[str, Any]:\n try:\n completion = await self.client.chat.completions.create(\n model=self.config.model,\n messages=messages,\n temperature=self.config.temperature,\n max_tokens=self.config.max_tokens\n )\n return {\"content\": completion.choices[0].message.content, \"tokens\": completion.usage.total_tokens}\n except Exception as e:\n logger.error(f\"LLM call failed: {e}\")\n return {\"content\": f\"Erreur: {str(e)}\", \"tokens\": 0}\n\nclass MultiAgentTeam:\n def __init__(self, agents: List[BaseAgent], orchestrator: str = \"sequential\"):\n self.agents = agents\n self.orchestrator = orchestrator\n \n async def execute_all(self, input_data: Dict[str, Any]) -> List[AgentResponse]:\n results = []\n for agent in self.agents:\n result = await agent.execute(input_data)\n results.append(result)\n return results\n"}, "cec8535cc766b8b211c8bee394ecca3c44b713a7": {"t": 1785749288.735945, "n": 3356, "h": "08ef0525238a404f", "c": "\"\"\"\nBase Agent Class for IA Factory\n\"\"\"\nfrom typing import List, Dict, Any, Optional, Callable\nfrom pydantic import BaseModel, Field\nfrom abc import ABC, abstractmethod\nfrom openai import AsyncOpenAI\nfrom anthropic import AsyncAnthropic\nimport logging\nfrom datetime import datetime\nimport os\n\nlogger = logging.getLogger(__name__)\n\nclass AgentConfig(BaseModel):\n name: str\n model: str = \"deepseek-chat\"\n temperature: float = 0.7\n max_tokens: int = 2000\n tools: List[str] = Field(default_factory=list)\n memory_enabled: bool = True\n language: str = \"fr\"\n system_prompt: Optional[str] = None\n\nclass AgentMessage(BaseModel):\n role: str\n content: str\n timestamp: datetime = Field(default_factory=datetime.now)\n metadata: Dict[str, Any] = Field(default_factory=dict)\n\nclass AgentResponse(BaseModel):\n content: str\n tools_used: List[str] = Field(default_factory=list)\n metadata: Dict[str, Any] = Field(default_factory=dict)\n tokens_used: Optional[int] = None\n duration_ms: Optional[float] = None\n\nclass BaseAgent(ABC):\n def __init__(self, config: AgentConfig):\n self.config = config\n self.memory: List[AgentMessage] = [] if config.memory_enabled else None\n self.tools_registry: Dict[str, Callable] = {}\n self.client = self._init_llm_client()\n logger.info(f\"Initialized {config.name} with model {config.model}\")\n \n def _init_llm_client(self):\n model = self.config.model.lower()\n if \"deepseek\" in model:\n return AsyncOpenAI(\n api_key=os.getenv(\"DEEPSEEK_API_KEY\"),\n base_url=\"https://api.deepseek.com\"\n )\n elif model.startswith(\"gpt\"):\n return AsyncOpenAI(api_key=os.getenv(\"OPENAI_API_KEY\"))\n elif model.startswith(\"claude\"):\n return AsyncAnthropic(api_key=os.getenv(\"ANTHROPIC_API_KEY\"))\n return AsyncOpenAI(api_key=os.getenv(\"DEEPSEEK_API_KEY\"), base_url=\"https://api.deepseek.com\")\n \n @abstractmethod\n async def execute(self, input_data: Dict[str, Any]) -> AgentResponse:\n pass\n \n def add_memory(self, message: AgentMessage):\n if self.memory is not None:\n self.memory.append(message)\n if len(self.memory) > 50:\n self.memory = self.memory[-50:]\n \n async def _call_llm(self, messages: List[Dict[str, str]]) -> Dict[str, Any]:\n try:\n completion = await self.client.chat.completions.create(\n model=self.config.model,\n messages=messages,\n temperature=self.config.temperature,\n max_tokens=self.config.max_tokens\n )\n return {\"content\": completion.choices[0].message.content, \"tokens\": completion.usage.total_tokens}\n except Exception as e:\n logger.error(f\"LLM call failed: {e}\")\n return {\"content\": f\"Erreur: {str(e)}\", \"tokens\": 0}\n\nclass MultiAgentTeam:\n def __init__(self, agents: List[BaseAgent], orchestrator: str = \"sequential\"):\n self.agents = agents\n self.orchestrator = orchestrator\n \n async def execute_all(self, input_data: Dict[str, Any]) -> List[AgentResponse]:\n results = []\n for agent in self.agents:\n result = await agent.execute(input_data)\n results.append(result)\n return results\n"}}, "services/api/app/llm_router/providers/deepseek_provider.py": {"7d6e298b582d583185cd24b914c17d9daeaa9491": {"t": 1785746391.4112089, "n": 2401, "h": "7c15bf020742b2f2", "c": "import requests\nimport time\nfrom typing import List\nfrom .base import BaseProvider, Message, LLMResponse\n\nclass DeepSeekProvider(BaseProvider):\n \"\"\"Provider for DeepSeek models (excellent for code)\"\"\"\n\n # Pricing per 1M tokens\n PRICING = {\n \"deepseek-chat\": 0.14,\n \"deepseek-coder\": 0.14\n }\n\n def __init__(self, api_key: str, model_name: str):\n super().__init__(api_key, model_name)\n self.api_url = \"https://api.deepseek.com/v1/chat/completions\"\n self.headers = {\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\"\n }\n\n def generate( # FIXED: removed async, using requests HTTP\n self,\n messages: List[Message],\n temperature: float = 0.7,\n max_tokens: int = 2000,\n **kwargs\n ) -> LLMResponse:\n \"\"\"Generate completion using DeepSeek\"\"\"\n start_time = time.time()\n\n # Format messages for DeepSeek API\n formatted_messages = self.format_messages(messages)\n\n # Prepare request payload\n payload = {\n \"model\": self.model_name,\n \"messages\": formatted_messages,\n \"temperature\": temperature,\n \"max_tokens\": max_tokens\n }\n\n # Call DeepSeek API via HTTP (avoids OpenAI SDK 'proxies' issue)\n response = requests.post(\n self.api_url,\n headers=self.headers,\n json=payload,\n timeout=30\n )\n\n latency_ms = int((time.time() - start_time) * 1000)\n\n # Check response\n if response.status_code != 200:\n raise Exception(f\"DeepSeek API error {response.status_code}: {response.text}\")\n\n data = response.json()\n\n # Extract response\n content = data[\"choices\"][0][\"message\"][\"content\"]\n tokens_used = data[\"usage\"][\"total_tokens\"]\n\n # Calculate cost\n cost = self.calculate_cost(tokens_used)\n\n return LLMResponse(\n content=content,\n model=self.model_name,\n provider=\"deepseek\",\n tokens_used=tokens_used,\n cost=cost,\n latency_ms=latency_ms\n )\n\n def calculate_cost(self, tokens_used: int) -> float:\n \"\"\"Calculate cost for DeepSeek based on pricing\"\"\"\n cost_per_1m = self.PRICING.get(self.model_name, 0.14)\n return (tokens_used / 1_000_000) * cost_per_1m\n"}, "cec8535cc766b8b211c8bee394ecca3c44b713a7": {"t": 1785749292.620687, "n": 2401, "h": "7c15bf020742b2f2", "c": "import requests\nimport time\nfrom typing import List\nfrom .base import BaseProvider, Message, LLMResponse\n\nclass DeepSeekProvider(BaseProvider):\n \"\"\"Provider for DeepSeek models (excellent for code)\"\"\"\n\n # Pricing per 1M tokens\n PRICING = {\n \"deepseek-chat\": 0.14,\n \"deepseek-coder\": 0.14\n }\n\n def __init__(self, api_key: str, model_name: str):\n super().__init__(api_key, model_name)\n self.api_url = \"https://api.deepseek.com/v1/chat/completions\"\n self.headers = {\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\"\n }\n\n def generate( # FIXED: removed async, using requests HTTP\n self,\n messages: List[Message],\n temperature: float = 0.7,\n max_tokens: int = 2000,\n **kwargs\n ) -> LLMResponse:\n \"\"\"Generate completion using DeepSeek\"\"\"\n start_time = time.time()\n\n # Format messages for DeepSeek API\n formatted_messages = self.format_messages(messages)\n\n # Prepare request payload\n payload = {\n \"model\": self.model_name,\n \"messages\": formatted_messages,\n \"temperature\": temperature,\n \"max_tokens\": max_tokens\n }\n\n # Call DeepSeek API via HTTP (avoids OpenAI SDK 'proxies' issue)\n response = requests.post(\n self.api_url,\n headers=self.headers,\n json=payload,\n timeout=30\n )\n\n latency_ms = int((time.time() - start_time) * 1000)\n\n # Check response\n if response.status_code != 200:\n raise Exception(f\"DeepSeek API error {response.status_code}: {response.text}\")\n\n data = response.json()\n\n # Extract response\n content = data[\"choices\"][0][\"message\"][\"content\"]\n tokens_used = data[\"usage\"][\"total_tokens\"]\n\n # Calculate cost\n cost = self.calculate_cost(tokens_used)\n\n return LLMResponse(\n content=content,\n model=self.model_name,\n provider=\"deepseek\",\n tokens_used=tokens_used,\n cost=cost,\n latency_ms=latency_ms\n )\n\n def calculate_cost(self, tokens_used: int) -> float:\n \"\"\"Calculate cost for DeepSeek based on pricing\"\"\"\n cost_per_1m = self.PRICING.get(self.model_name, 0.14)\n return (tokens_used / 1_000_000) * cost_per_1m\n"}}, "apps/_archived/council/index_server.html": {"7d6e298b582d583185cd24b914c17d9daeaa9491": {"t": 1785746405.0088425, "n": 58274, "h": "f1d4d1fa1eed03d7", "c": "<!DOCTYPE html>\n<html lang=\"fr\" data-theme=\"dark\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Build Your Council - IA Factory Algeria</title>\n <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n <link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap\" rel=\"stylesheet\">\n <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css\">\n <style>\n :root {\n /* Dark Mode (default) - IAFactory Theme */\n --bg-primary: #020617;\n --bg-secondary: #0a0f1a;\n --bg-card: #0f1629;\n --bg-card-hover: #151d30;\n --border-color: rgba(255, 255, 255, 0.12);\n --text-primary: #f8fafc;\n --text-secondary: rgba(248, 250, 252, 0.75);\n --text-muted: rgba(248, 250, 252, 0.5);\n --accent-primary: #00a651;\n --accent-secondary: #00c95d;\n --accent-gradient: linear-gradient(135deg, #00a651, #008c45);\n --success: #10b981;\n --success-bg: rgba(16, 185, 129, 0.1);\n --warning: #f59e0b;\n --warning-bg: rgba(245, 158, 11, 0.1);\n --error: #ef4444;\n --input-bg: #0a0f1a;\n --input-border: rgba(255, 255, 255, 0.12);\n --input-text: #f8fafc;\n --input-placeholder: rgba(248, 250, 252, 0.5);\n }\n\n [data-theme=\"light\"] {\n --bg-primary: #f7f5f0;\n --bg-secondary: #efe9df;\n --bg-card: #ffffff;\n --bg-card-hover: #f7f5f0;\n --border-color: rgba(0, 0, 0, 0.08);\n --text-primary: #0f172a;\n --text-secondary: rgba(15, 23, 42, 0.7);\n --text-muted: rgba(15, 23, 42, 0.5);\n --accent-primary: #00a651;\n --accent-secondary: #008c45;\n --accent-gradient: linear-gradient(135deg, #00a651, #008c45);\n --success-bg: rgba(16, 185, 129, 0.1);\n --warning-bg: rgba(245, 158, 11, 0.1);\n --input-bg: #ffffff;\n --input-border: rgba(0, 0, 0, 0.12);\n --input-text: #0f172a;\n --input-placeholder: rgba(15, 23, 42, 0.5);\n }\n\n * {\n font-family: 'Inter', sans-serif;\n margin: 0;\n padding: 0;\n box-sizing: border-box;\n }\n\n body {\n background: var(--bg-primary);\n color: var(--text-primary);\n min-height: 100vh;\n transition: background 0.3s ease, color 0.3s ease;\n }\n\n /* Header */\n .main-header {\n background: var(--bg-secondary);\n border-bottom: 1px solid var(--border-color);\n padding: 1rem 2rem;\n display: flex;\n justify-content: space-between;\n align-items: center;\n position: sticky;\n top: 0;\n z-index: 100;\n }\n\n .logo {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n }\n\n .logo-icon {\n width: 40px;\n height: 40px;\n background: var(--accent-gradient);\n border-radius: 10px;\n display: flex;\n align-items: center;\n justify-content: center;\n color: white;\n font-size: 1.25rem;\n }\n\n .logo h1 {\n font-size: 1.25rem;\n font-weight: 700;\n background: var(--accent-gradient);\n -webkit-background-clip: text;\n -webkit-text-fill-color: transparent;\n }\n\n .header-actions {\n display: flex;\n align-items: center;\n gap: 1rem;\n }\n\n .theme-toggle {\n background: var(--bg-card);\n border: 1px solid var(--border-color);\n color: var(--text-primary);\n padding: 0.5rem 0.75rem;\n border-radius: 8px;\n cursor: pointer;\n font-size: 1rem;\n transition: all 0.2s;\n }\n\n .theme-toggle:hover {\n background: var(--bg-card-hover);\n border-color: var(--accent-primary);\n }\n\n .home-btn {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n padding: 0.5rem 1rem;\n background: var(--accent-gradient);\n color: white;\n border: none;\n border-radius: 8px;\n font-size: 0.875rem;\n font-weight: 500;\n cursor: pointer;\n text-decoration: none;\n transition: all 0.2s;\n }\n\n .home-btn:hover {\n transform: translateY(-1px);\n box-shadow: 0 4px 12px rgba(124, 58, 237, 0.3);\n }\n\n /* Main Container */\n .container {\n max-width: 1400px;\n margin: 0 auto;\n padding: 2rem;\n }\n\n /* Page Header */\n .page-header {\n text-align: center;\n margin-bottom: 2rem;\n }\n\n .page-header h1 {\n font-size: 2.5rem;\n font-weight: 800;\n margin-bottom: 0.5rem;\n background: var(--accent-gradient);\n -webkit-background-clip: text;\n -webkit-text-fill-color: transparent;\n }\n\n .page-header p {\n font-size: 1.1rem;\n color: var(--text-secondary);\n }\n\n /* Presets */\n .presets-grid {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));\n gap: 1rem;\n margin: 1.5rem 0;\n }\n\n .preset-card {\n padding: 1.25rem;\n background: var(--bg-card);\n border: 2px solid var(--border-color);\n border-radius: 12px;\n cursor: pointer;\n transition: all 0.2s;\n text-align: center;\n }\n\n .preset-card:hover {\n transform: translateY(-2px);\n border-color: var(--accent-primary);\n box-shadow: 0 4px 12px rgba(124, 58, 237, 0.15);\n }\n\n .preset-card.active {\n border-color: var(--accent-primary);\n background: rgba(124, 58, 237, 0.1);\n }\n\n .preset-icon {\n font-size: 2rem;\n margin-bottom: 0.5rem;\n }\n\n .preset-name {\n font-weight: 600;\n margin-bottom: 0.25rem;\n color: var(--text-primary);\n }\n\n .preset-desc {\n font-size: 0.75rem;\n color: var(--text-secondary);\n }\n\n /* Config Sections */\n .config-section {\n background: var(--bg-card);\n border: 1px solid var(--border-color);\n border-radius: 16px;\n padding: 1.5rem;\n margin-bottom: 1.5rem;\n }\n\n .section-title {\n font-size: 1.125rem;\n font-weight: 700;\n margin-bottom: 1.5rem;\n display: flex;\n align-items: center;\n gap: 0.5rem;\n color: var(--text-primary);\n }\n\n /* Experts Grid */\n .experts-grid {\n display: grid;\n grid-template-columns: repeat(4, 1fr);\n gap: 1.5rem;\n }\n\n @media (max-width: 1200px) {\n .experts-grid {\n grid-template-columns: repeat(2, 1fr);\n }\n }\n\n @media (max-width: 600px) {\n .experts-grid {\n grid-template-columns: 1fr;\n }\n }\n\n .expert-card {\n background: var(--bg-secondary);\n border: 2px solid var(--border-color);\n border-radius: 12px;\n padding: 1.25rem;\n transition: all 0.2s;\n }\n\n .expert-card:hover {\n border-color: var(--accent-secondary);\n }\n\n .expert-card.expert-1 { border-top: 3px solid var(--accent-primary); }\n .expert-card.expert-2 { border-top: 3px solid var(--accent-primary); }\n .expert-card.expert-3 { border-top: 3px solid var(--accent-primary); }\n .expert-card.chairman { border-top: 3px solid var(--accent-secondary); }\n\n .expert-label {\n font-weight: 700;\n margin-bottom: 1rem;\n font-size: 0.95rem;\n display: flex;\n align-items: center;\n gap: 0.5rem;\n }\n\n .expert-1 .expert-label { color: var(--accent-primary); }\n .expert-2 .expert-label { color: var(--accent-primary); }\n .expert-3 .expert-label { color: var(--accent-primary); }\n .chairman .expert-label { color: var(--accent-secondary); }\n\n .form-group {\n margin-bottom: 1rem;\n }\n\n .form-group:last-child {\n margin-bottom: 0;\n }\n\n .form-group label {\n display: block;\n font-size: 0.75rem;\n font-weight: 600;\n color: var(--text-secondary);\n margin-bottom: 0.4rem;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n }\n\n .form-group select {\n width: 100%;\n padding: 0.75rem 1rem;\n background: var(--input-bg);\n border: 1px solid var(--input-border);\n border-radius: 8px;\n font-size: 0.875rem;\n color: var(--input-text);\n cursor: pointer;\n transition: all 0.2s;\n appearance: none;\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%2394a3b8'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E\");\n background-repeat: no-repeat;\n background-position: right 0.75rem center;\n background-size: 1rem;\n padding-right: 2.5rem;\n }\n\n .form-group select:focus {\n outline: none;\n border-color: var(--accent-primary);\n box-shadow: 0 0 0 3px rgba(124, 58, 237, 0.15);\n }\n\n .form-group select option {\n background: var(--bg-card);\n color: var(--text-primary);\n padding: 0.5rem;\n }\n\n .strengths-box {\n font-size: 0.8rem;\n color: var(--text-secondary);\n padding: 0.5rem 0.75rem;\n background: rgba(0, 166, 81, 0.1);\n border-radius: 6px;\n margin-top: 0.75rem;\n }\n\n /* Estimate Box */\n .estimate-box {\n background: var(--accent-gradient);\n color: white;\n padding: 1.5rem;\n border-radius: 12px;\n margin-bottom: 1.5rem;\n }\n\n .estimate-title {\n font-size: 1.1rem;\n font-weight: 700;\n margin-bottom: 1rem;\n }\n\n .estimate-grid {\n display: grid;\n grid-template-columns: repeat(3, 1fr);\n gap: 1rem;\n }\n\n @media (max-width: 600px) {\n .estimate-grid {\n grid-template-columns: 1fr;\n }\n }\n\n .estimate-item {\n background: rgba(255,255,255,0.15);\n padding: 1rem;\n border-radius: 8px;\n text-align: center;\n }\n\n .estimate-label {\n font-size: 0.85rem;\n opacity: 0.9;\n }\n\n .estimate-value {\n font-size: 1.5rem;\n font-weight: 700;\n margin-top: 0.25rem;\n }\n\n .warning-box {\n display: none;\n margin-top: 1rem;\n padding: 0.75rem;\n background: rgba(245, 158, 11, 0.2);\n border-radius: 8px;\n font-size: 0.875rem;\n }\n\n /* Query Section */\n .query-section textarea {\n width: 100%;\n min-height: 150px;\n padding: 1rem;\n background: var(--input-bg);\n border: 2px solid var(--input-border);\n border-radius: 12px;\n font-size: 0.95rem;\n color: var(--input-text);\n resize: vertical;\n transition: all 0.2s;\n line-height: 1.6;\n }\n\n .query-section textarea::placeholder {\n color: var(--input-placeholder);\n }\n\n .query-section textarea:focus {\n outline: none;\n border-color: var(--accent-primary);\n box-shadow: 0 0 0 3px rgba(124, 58, 237, 0.15);\n }\n\n /* Options Row */\n .options-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n margin-top: 1rem;\n flex-wrap: wrap;\n gap: 1rem;\n }\n\n .checkbox-label {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n cursor: pointer;\n font-size: 0.9rem;\n color: var(--text-secondary);\n }\n\n .checkbox-label input[type=\"checkbox\"] {\n width: 1.25rem;\n height: 1.25rem;\n cursor: pointer;\n accent-color: var(--accent-primary);\n }\n\n /* Submit Button */\n .btn-submit {\n padding: 1rem 2rem;\n background: var(--accent-gradient);\n color: white;\n border: none;\n border-radius: 10px;\n font-size: 1rem;\n font-weight: 600;\n cursor: pointer;\n transition: all 0.2s;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 0.5rem;\n }\n\n .btn-submit:hover:not(:disabled) {\n transform: translateY(-1px);\n box-shadow: 0 6px 20px rgba(124, 58, 237, 0.35);\n }\n\n .btn-submit:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n }\n\n /* Loading */\n .loading-box {\n display: none;\n padding: 1.5rem;\n background: var(--warning-bg);\n border-left: 4px solid var(--warning);\n border-radius: 8px;\n margin-bottom: 1.5rem;\n }\n\n .loading-content {\n display: flex;\n align-items: center;\n gap: 1rem;\n }\n\n .spinner {\n border: 3px solid rgba(245, 158, 11, 0.3);\n border-top-color: var(--warning);\n border-radius: 50%;\n width: 24px;\n height: 24px;\n animation: spin 0.8s linear infinite;\n }\n\n @keyframes spin {\n to { transform: rotate(360deg); }\n }\n\n /* Results Section */\n .results-section {\n display: none;\n }\n\n .final-response-box {\n background: linear-gradient(135deg, rgba(16, 185, 129, 0.15), rgba(5, 150, 105, 0.1));\n border: 1px solid rgba(16, 185, 129, 0.3);\n border-left: 4px solid var(--success);\n border-radius: 16px;\n padding: 2rem;\n margin-bottom: 1.5rem;\n }\n\n .result-header {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n margin-bottom: 1rem;\n font-size: 1.25rem;\n font-weight: 700;\n color: var(--success);\n }\n\n .result-text {\n font-size: 1rem;\n line-height: 1.8;\n color: var(--text-primary);\n white-space: pre-wrap;\n }\n\n .result-meta {\n margin-top: 1.5rem;\n padding-top: 1rem;\n border-top: 1px solid rgba(16, 185, 129, 0.2);\n display: flex;\n gap: 2rem;\n font-size: 0.875rem;\n color: var(--text-secondary);\n }\n\n /* Opinions Section */\n .opinions-box {\n background: var(--bg-card);\n border: 1px solid var(--border-color);\n border-radius: 16px;\n padding: 1.5rem;\n }\n\n .opinion-tabs {\n display: flex;\n gap: 0.5rem;\n margin-bottom: 1.5rem;\n border-bottom: 2px solid var(--border-color);\n padding-bottom: 0.5rem;\n overflow-x: auto;\n }\n\n .opinion-tab {\n padding: 0.75rem 1.25rem;\n background: transparent;\n border: none;\n border-bottom: 3px solid transparent;\n cursor: pointer;\n font-weight: 500;\n color: var(--text-secondary);\n transition: all 0.2s;\n white-space: nowrap;\n }\n\n .opinion-tab:hover {\n color: var(--text-primary);\n }\n\n .opinion-tab.active {\n color: var(--accent-primary);\n border-bottom-color: var(--accent-primary);\n }\n\n .opinion-content {\n padding: 1.5rem;\n background: var(--bg-secondary);\n border-radius: 12px;\n line-height: 1.8;\n white-space: pre-wrap;\n color: var(--text-primary);\n max-height: 400px;\n overflow-y: auto;\n }\n\n /* Error Toast */\n .error-toast {\n position: fixed;\n bottom: 2rem;\n right: 2rem;\n padding: 1rem 1.5rem;\n background: var(--error);\n color: white;\n border-radius: 10px;\n font-size: 0.875rem;\n display: none;\n z-index: 1000;\n animation: slideIn 0.3s ease;\n max-width: 400px;\n }\n\n .error-toast.visible {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n }\n\n @keyframes slideIn {\n from {\n transform: translateY(20px);\n opacity: 0;\n }\n to {\n transform: translateY(0);\n opacity: 1;\n }\n }\n\n /* Scrollbar */\n ::-webkit-scrollbar {\n width: 8px;\n height: 8px;\n }\n\n ::-webkit-scrollbar-track {\n background: transparent;\n }\n\n ::-webkit-scrollbar-thumb {\n background: var(--border-color);\n border-radius: 4px;\n }\n\n ::-webkit-scrollbar-thumb:hover {\n background: var(--text-muted);\n }\n </style>\n\n<!-- IAFactory i18n System - TRILINGUE -->\n<link rel=\"stylesheet\" href=\"/shared/iafactory-theme.css\">\n<script src=\"/shared/i18n.js\"></script>\n<script src=\"/shared/language-switcher.js\"></script>\n\n</head>\n<body>\n <!-- Header -->\n <header class=\"main-header\">\n <div class=\"logo\">\n <div class=\"logo-icon\">\n <i class=\"fas fa-users\"></i>\n </div>\n <h1>Council IA</h1>\n </div>\n <div class=\"header-actions\">\n\n<!-- Language Switcher -->\n<div data-language-switcher></div>\n\n <button class=\"theme-toggle\" onclick=\"toggleTheme()\" title=\"Changer le thème\">\n <i class=\"fas fa-moon\" id=\"theme-icon\"></i>\n </button>\n <a href=\"/\" class=\"home-btn\">\n <i class=\"fas fa-home\"></i>\n Accueil\n </a>\n </div>\n </header>\n\n <div class=\"container\">\n <!-- Page Header -->\n <div class=\"page-header\">\n <h1>🎯 Build Your Council</h1>\n <p>Composez votre équipe d'experts IA sur mesure avec une infinité de combinaisons</p>\n </div>\n\n <!-- Presets -->\n <div class=\"config-section\">\n <h2 class=\"section-title\">✨ Configurations recommandées (Groq ⚡ + OpenRouter 🌐)</h2>\n <div class=\"presets-grid\" id=\"presetsGrid\">\n <div class=\"preset-card\" onclick=\"applyPreset('fast')\" style=\"border: 2px solid var(--accent-primary);\">\n <div class=\"preset-icon\">⚡</div>\n <div class=\"preset-name\">Groq Diversifié ⭐</div>\n <div class=\"preset-desc\">Llama 70B + Qwen + Kimi</div>\n </div>\n <div class=\"preset-card\" onclick=\"applyPreset('deepseek')\" style=\"border: 2px solid var(--accent-primary);\">\n <div class=\"preset-icon\">🇨🇳</div>\n <div class=\"preset-name\">DeepSeek Power ⭐</div>\n <div class=\"preset-desc\">Chat + R1 Reasoner</div>\n </div>\n <div class=\"preset-card\" onclick=\"applyPreset('diversified')\">\n <div class=\"preset-icon\">🎯</div>\n <div class=\"preset-name\">Mix Groq+DeepSeek</div>\n <div class=\"preset-desc\">Llama + Kimi + DeepSeek</div>\n </div>\n <div class=\"preset-card\" onclick=\"applyPreset('llama4')\">\n <div class=\"preset-icon\">🦙</div>\n <div class=\"preset-name\">Llama 4 Meta</div>\n <div class=\"preset-desc\">Maverick + Scout + 70B</div>\n </div>\n <div class=\"preset-card\" onclick=\"applyPreset('openrouter')\">\n <div class=\"preset-icon\">🌐</div>\n <div class=\"preset-name\">OpenRouter FREE</div>\n <div class=\"preset-desc\">Gemini + Llama + Mistral</div>\n </div>\n <div class=\"preset-card\" onclick=\"applyPreset('hybrid')\">\n <div class=\"preset-icon\">🔀</div>\n <div class=\"preset-name\">Hybride</div>\n <div class=\"preset-desc\">Groq + Gemini + Qwen</div>\n </div>\n <div class=\"preset-card\" onclick=\"applyPreset('local')\">\n <div class=\"preset-icon\">🏠</div>\n <div class=\"preset-name\">100% Local</div>\n <div class=\"preset-desc\">Ollama (lent sur VPS)</div>\n </div>\n </div>\n </div>\n\n <!-- Configuration -->\n <div class=\"config-section\">\n <h2 class=\"section-title\">⚙️ Configuration du Council</h2>\n \n <div class=\"experts-grid\">\n <!-- Expert 1 -->\n <div class=\"expert-card expert-1\">\n <div class=\"expert-label\">\n <i class=\"fas fa-user-tie\"></i> Expert 1\n </div>\n <div class=\"form-group\">\n <label>Provider</label>\n <select id=\"provider1\" onchange=\"onProviderChange(1)\">\n <option value=\"deepseek\">🇨🇳 DeepSeek (Économique)</option>\n <option value=\"groq\" selected>⚡ Groq (Ultra-rapide)</option>\n <option value=\"openrouter\">🌐 OpenRouter</option>\n <option value=\"ollama\">🏠 Ollama (Local - lent)</option>\n </select>\n </div>\n <div class=\"form-group\">\n <label>Modèle</label>\n <select id=\"model1\"></select>\n </div>\n <div class=\"strengths-box\" id=\"strengths1\">💪 Sélectionnez un modèle</div>\n </div>\n\n <!-- Expert 2 -->\n <div class=\"expert-card expert-2\">\n <div class=\"expert-label\">\n <i class=\"fas fa-user-tie\"></i> Expert 2\n </div>\n <div class=\"form-group\">\n <label>Provider</label>\n <select id=\"provider2\" onchange=\"onProviderChange(2)\">\n <option value=\"deepseek\">🇨🇳 DeepSeek (Économique)</option>\n <option value=\"groq\" selected>⚡ Groq (Ultra-rapide)</option>\n <option value=\"openrouter\">🌐 OpenRouter</option>\n <option value=\"ollama\">🏠 Ollama (Local - lent)</option>\n </select>\n </div>\n <div class=\"form-group\">\n <label>Modèle</label>\n <select id=\"model2\"></select>\n </div>\n <div class=\"strengths-box\" id=\"strengths2\">💪 Sélectionnez un modèle</div>\n </div>\n\n <!-- Expert 3 -->\n <div class=\"expert-card expert-3\">\n <div class=\"expert-label\">\n <i class=\"fas fa-user-tie\"></i> Expert 3\n </div>\n <div class=\"form-group\">\n <label>Provider</label>\n <select id=\"provider3\" onchange=\"onProviderChange(3)\">\n <option value=\"deepseek\">🇨🇳 DeepSeek (Économique)</option>\n <option value=\"groq\" selected>⚡ Groq (Ultra-rapide)</option>\n <option value=\"openrouter\">🌐 OpenRouter</option>\n <option value=\"ollama\">🏠 Ollama (Local - lent)</option>\n </select>\n </div>\n <div class=\"form-group\">\n <label>Modèle</label>\n <select id=\"model3\"></select>\n </div>\n <div class=\"strengths-box\" id=\"strengths3\">💪 Sélectionnez un modèle</div>\n </div>\n\n <!-- Chairman -->\n <div class=\"expert-card chairman\">\n <div class=\"expert-label\">\n <i class=\"fas fa-crown\"></i> Chairman (Synthèse)\n </div>\n <div class=\"form-group\">\n <label>Provider</label>\n <select id=\"providerChairman\" onchange=\"onProviderChange('Chairman')\">\n <option value=\"deepseek\">🇨🇳 DeepSeek (Économique)</option>\n <option value=\"groq\" selected>⚡ Groq (Ultra-rapide)</option>\n <option value=\"openrouter\">🌐 OpenRouter</option>\n <option value=\"ollama\">🏠 Ollama (Local - lent)</option>\n </select>\n </div>\n <div class=\"form-group\">\n <label>Modèle</label>\n <select id=\"modelChairman\"></select>\n </div>\n <div class=\"strengths-box\" id=\"strengthsChairman\">💪 Sélectionnez un modèle</div>\n </div>\n </div>\n </div>\n\n <!-- Estimation -->\n <div class=\"estimate-box\">\n <div class=\"estimate-title\">📊 Estimation</div>\n <div class=\"estimate-grid\">\n <div class=\"estimate-item\">\n <div class=\"estimate-label\">💰 Coût estimé</div>\n <div class=\"estimate-value\" id=\"costEstimate\">~50 DZD</div>\n </div>\n <div class=\"estimate-item\">\n <div class=\"estimate-label\">⏱️ Temps estimé</div>\n <div class=\"estimate-value\" id=\"timeEstimate\">~15s</div>\n </div>\n <div class=\"estimate-item\">\n <div class=\"estimate-label\">🎯 Combinaisons</div>\n <div class=\"estimate-value\">∞</div>\n </div>\n </div>\n <div class=\"warning-box\" id=\"warningBox\">\n ⚠️ Configuration premium : coût élevé. Utilisez Groq pour réduire les coûts.\n </div>\n </div>\n\n <!-- Query Section -->\n <div class=\"config-section query-section\">\n <h2 class=\"section-title\">💬 Votre question</h2>\n <textarea \n id=\"queryInput\" \n placeholder=\"Posez votre question au Council personnalisé...\n\nExemples:\n• Quelles sont les meilleures pratiques pour sécuriser une API REST ?\n• Comment optimiser PostgreSQL pour des millions de lignes ?\n• Expliquez la différence entre authentification et autorisation\n• Compare les architectures microservices vs monolithique\"\n ></textarea>\n \n <div class=\"options-row\">\n <label class=\"checkbox-label\">\n <input type=\"checkbox\" id=\"enableReview\">\n <span>Activer la revue croisée (+50% temps, meilleure qualité)</span>\n </label>\n <button class=\"btn-submit\" id=\"submitBtn\" onclick=\"executeCouncil()\">\n <i class=\"fas fa-bolt\"></i>\n Lancer le Council\n </button>\n </div>\n </div>\n\n <!-- Loading -->\n <div class=\"loading-box\" id=\"loadingBox\">\n <div class=\"loading-content\">\n <div class=\"spinner\"></div>\n <div>\n <div style=\"font-weight: 600; margin-bottom: 0.25rem;\">Council en délibération...</div>\n <div style=\"font-size: 0.875rem; color: var(--text-secondary);\" id=\"loadingStatus\">Les experts analysent votre question</div>\n </div>\n </div>\n </div>\n\n <!-- Results -->\n <div class=\"results-section\" id=\"resultsSection\">\n <div class=\"final-response-box\">\n <div class=\"result-header\">\n <i class=\"fas fa-crown\"></i>\n <span>Réponse finale du Chairman</span>\n <span id=\"chairmanName\" style=\"font-size: 0.9rem; font-weight: normal; color: var(--text-secondary);\"></span>\n </div>\n <div class=\"result-text\" id=\"finalResponse\"></div>\n <div class=\"result-meta\">\n <span id=\"execTime\"><i class=\"fas fa-clock\"></i> -</span>\n <span id=\"actualCost\"><i class=\"fas fa-coins\"></i> -</span>\n <span id=\"expertsCount\"><i class=\"fas fa-users\"></i> -</span>\n </div>\n </div>\n\n <div class=\"opinions-box\">\n <h3 class=\"section-title\">🎯 Opinions individuelles des experts</h3>\n <div class=\"opinion-tabs\" id=\"opinionTabs\"></div>\n <div class=\"opinion-content\" id=\"opinionContent\">Sélectionnez un expert pour voir son opinion</div>\n </div>\n </div>\n </div>\n\n <!-- Error Toast -->\n <div class=\"error-toast\" id=\"errorToast\">\n <i class=\"fas fa-exclamation-circle\"></i>\n <span id=\"errorMessage\"></span>\n </div>\n\n <script>\n // ============================================\n // Provider and Model Configuration\n // ============================================\n // API Keys pour appels directs\n const OPENROUTER_API_KEY = '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';\n const GROQ_API_KEY = 'YOUR_GROQ_API_KEY';\n const DEEPSEEK_API_KEY = 'sk-e2d7d214600946479856ffafbe1ce392';\n\n const PROVIDERS = {\n deepseek: {\n name: 'DeepSeek 🇨🇳',\n models: [\n { id: 'deepseek.deepseek-chat', name: 'DeepSeek Chat V3', strengths: ['Économique', 'Raisonnement', 'Code'], cost: 1 },\n { id: 'deepseek.deepseek-reasoner', name: 'DeepSeek R1 🧠', strengths: ['Raisonnement avancé', 'Math', 'Logique'], cost: 3 }\n ]\n },\n groq: {\n name: 'Groq ⚡',\n models: [\n { id: 'groq.llama-3.3-70b-versatile', name: 'Llama 3.3 70B', strengths: ['Puissant', 'Versatile', '131K ctx'], cost: 0 },\n { id: 'groq.llama-3.1-8b-instant', name: 'Llama 3.1 8B', strengths: ['Ultra-rapide', 'Gratuit'], cost: 0 },\n { id: 'groq.moonshotai/kimi-k2-instruct', name: 'Kimi K2 🌙', strengths: ['Moonshot AI', 'Raisonnement'], cost: 0 },\n { id: 'groq.qwen/qwen3-32b', name: 'Qwen3 32B', strengths: ['Alibaba', 'Multilingue', 'Code'], cost: 0 },\n { id: 'groq.meta-llama/llama-4-maverick-17b-128e-instruct', name: 'Llama 4 Maverick 🦙', strengths: ['Meta', 'Nouveau', '131K ctx'], cost: 0 },\n { id: 'groq.meta-llama/llama-4-scout-17b-16e-instruct', name: 'Llama 4 Scout 🦙', strengths: ['Meta', 'Exploration'], cost: 0 },\n { id: 'groq.openai/gpt-oss-120b', name: 'GPT-OSS 120B', strengths: ['OpenAI OSS', 'Très puissant'], cost: 0 },\n { id: 'groq.openai/gpt-oss-20b', name: 'GPT-OSS 20B', strengths: ['OpenAI OSS', 'Rapide'], cost: 0 },\n { id: 'groq.allam-2-7b', name: 'Allam 2 7B 🇸🇦', strengths: ['Arabe natif', 'SDAIA'], cost: 0 }\n ]\n },\n openrouter: {\n name: 'OpenRouter 🌐',\n models: [\n { id: 'openrouter.google/gemini-2.0-flash-exp:free', name: 'Gemini 2.0 Flash (Free)', strengths: ['Gratuit', 'Google', 'Rapide'], cost: 0 },\n { id: 'openrouter.meta-llama/llama-3.2-3b-instruct:free', name: 'Llama 3.2 3B (Free)', strengths: ['Gratuit', 'Meta', 'Compact'], cost: 0 },\n { id: 'openrouter.mistralai/mistral-7b-instruct:free', name: 'Mistral 7B (Free)', strengths: ['Gratuit', 'FR natif'], cost: 0 },\n { id: 'openrouter.deepseek/deepseek-chat', name: 'DeepSeek Chat 🇨🇳', strengths: ['Économique', 'Code', 'Raisonnement'], cost: 2 },\n { id: 'openrouter.deepseek/deepseek-r1', name: 'DeepSeek R1 🧠', strengths: ['Raisonnement avancé', 'Math'], cost: 5 },\n { id: 'openrouter.qwen/qwen-2.5-72b-instruct', name: 'Qwen 2.5 72B', strengths: ['Alibaba', 'Puissant'], cost: 10 },\n { id: 'openrouter.anthropic/claude-3.5-sonnet', name: 'Claude 3.5 Sonnet', strengths: ['Anthropic', 'Écriture'], cost: 15 },\n { id: 'openrouter.openai/gpt-4o-mini', name: 'GPT-4o Mini', strengths: ['OpenAI', 'Rapide'], cost: 5 },\n { id: 'openrouter.openai/gpt-4o', name: 'GPT-4o', strengths: ['OpenAI', 'Meilleur'], cost: 25 }\n ]\n },\n ollama: {\n name: 'Ollama 🏠 (lent)',\n models: [\n { id: 'ollama.llama3.2:3b', name: 'Llama 3.2 3B', strengths: ['Local', 'Privé', 'Rapide'], cost: 0 },\n { id: 'ollama.mistral:7b', name: 'Mistral 7B', strengths: ['Local', 'FR natif', 'Équilibré'], cost: 0 },\n { id: 'ollama.qwen2.5:3b', name: 'Qwen 2.5 3B', strengths: ['Local', 'Multilingue', 'Compact'], cost: 0 }\n ]\n }\n };\n\n // NOTE: ⚡ = Groq ultra-rapide, 🌐 = OpenRouter (gratuit/payant), 🏠 = Local (lent sur VPS)\n\n const PRESETS = {\n openrouter: {\n name: 'OpenRouter Free',\n experts: [\n { provider: 'openrouter', model: 'openrouter.google/gemini-2.0-flash-exp:free' },\n { provider: 'openrouter', model: 'openrouter.meta-llama/llama-3.2-3b-instruct:free' },\n { provider: 'openrouter', model: 'openrouter.mistralai/mistral-7b-instruct:free' }\n ],\n chairman: { provider: 'openrouter', model: 'openrouter.google/gemini-2.0-flash-exp:free' }\n },\n fast: {\n name: 'Groq Ultra Rapide',\n experts: [\n { provider: 'groq', model: 'groq.llama-3.3-70b-versatile' },\n { provider: 'groq', model: 'groq.qwen/qwen3-32b' },\n { provider: 'groq', model: 'groq.moonshotai/kimi-k2-instruct' }\n ],\n chairman: { provider: 'groq', model: 'groq.llama-3.3-70b-versatile' }\n },\n diversified: {\n name: 'Diversifié',\n experts: [\n { provider: 'groq', model: 'groq.llama-3.3-70b-versatile' },\n { provider: 'groq', model: 'groq.moonshotai/kimi-k2-instruct' },\n { provider: 'deepseek', model: 'deepseek.deepseek-chat' }\n ],\n chairman: { provider: 'deepseek', model: 'deepseek.deepseek-reasoner' }\n },\n deepseek: {\n name: 'DeepSeek Power 🇨🇳',\n experts: [\n { provider: 'deepseek', model: 'deepseek.deepseek-chat' },\n { provider: 'deepseek', model: 'deepseek.deepseek-reasoner' },\n { provider: 'groq', model: 'groq.llama-3.3-70b-versatile' }\n ],\n chairman: { provider: 'deepseek', model: 'deepseek.deepseek-reasoner' }\n },\n llama4: {\n name: 'Llama 4 🦙',\n experts: [\n { provider: 'groq', model: 'groq.meta-llama/llama-4-maverick-17b-128e-instruct' },\n { provider: 'groq', model: 'groq.meta-llama/llama-4-scout-17b-16e-instruct' },\n { provider: 'groq', model: 'groq.llama-3.3-70b-versatile' }\n ],\n chairman: { provider: 'groq', model: 'groq.meta-llama/llama-4-maverick-17b-128e-instruct' }\n },\n hybrid: {\n name: 'Hybride',\n experts: [\n { provider: 'groq', model: 'groq.llama-3.3-70b-versatile' },\n { provider: 'openrouter', model: 'openrouter.google/gemini-2.0-flash-exp:free' },\n { provider: 'groq', model: 'groq.qwen/qwen3-32b' }\n ],\n chairman: { provider: 'groq', model: 'groq.moonshotai/kimi-k2-instruct' }\n },\n local: {\n name: '100% Local',\n experts: [\n { provider: 'ollama', model: 'ollama.llama3.2:3b' },\n { provider: 'ollama', model: 'ollama.mistral:7b' },\n { provider: 'ollama', model: 'ollama.qwen2.5:3b' }\n ],\n chairman: { provider: 'ollama', model: 'ollama.mistral:7b' }\n }\n };\n\n let currentOpinions = {};\n\n // ============================================\n // Initialization\n // ============================================\n document.addEventListener('DOMContentLoaded', () => {\n // Load theme\n const savedTheme = localStorage.getItem('council-theme') || 'dark';\n document.documentElement.setAttribute('data-theme', savedTheme);\n updateThemeIcon(savedTheme);\n\n // Initialize all expert selects\n [1, 2, 3, 'Chairman'].forEach(num => {\n onProviderChange(num);\n });\n\n updateEstimate();\n });\n\n // ============================================\n // Theme Toggle\n // ============================================\n function toggleTheme() {\n const html = document.documentElement;\n const currentTheme = html.getAttribute('data-theme');\n const newTheme = currentTheme === 'dark' ? 'light' : 'dark';\n html.setAttribute('data-theme', newTheme);\n localStorage.setItem('council-theme', newTheme);\n updateThemeIcon(newTheme);\n }\n\n function updateThemeIcon(theme) {\n const icon = document.getElementById('theme-icon');\n icon.className = theme === 'dark' ? 'fas fa-moon' : 'fas fa-sun';\n }\n\n // ============================================\n // Provider/Model Management\n // ============================================\n function onProviderChange(expertNum) {\n const providerSelect = document.getElementById(`provider${expertNum}`);\n const modelSelect = document.getElementById(`model${expertNum}`);\n const strengthsBox = document.getElementById(`strengths${expertNum}`);\n \n const provider = providerSelect.value;\n const models = PROVIDERS[provider].models;\n\n // Populate models\n modelSelect.innerHTML = models.map(m => \n `<option value=\"${m.id}\">${m.name}</option>`\n ).join('');\n\n // Update strengths\n updateStrengths(expertNum);\n \n // Add change listener to model\n modelSelect.onchange = () => updateStrengths(expertNum);\n\n updateEstimate();\n }\n\n function updateStrengths(expertNum) {\n const providerSelect = document.getElementById(`provider${expertNum}`);\n const modelSelect = document.getElementById(`model${expertNum}`);\n const strengthsBox = document.getElementById(`strengths${expertNum}`);\n\n const provider = providerSelect.value;\n const modelId = modelSelect.value;\n const model = PROVIDERS[provider].models.find(m => m.id === modelId);\n\n if (model) {\n strengthsBox.textContent = `💪 ${model.strengths.join(', ')}`;\n }\n\n updateEstimate();\n }\n\n // ============================================\n // Presets\n // ============================================\n function applyPreset(presetName) {\n const preset = PRESETS[presetName];\n \n // Update presets UI\n document.querySelectorAll('.preset-card').forEach(card => {\n card.classList.remove('active');\n });\n event.currentTarget.classList.add('active');\n\n // Apply expert configs\n preset.experts.forEach((exp, idx) => {\n const num = idx + 1;\n document.getElementById(`provider${num}`).value = exp.provider;\n onProviderChange(num);\n document.getElementById(`model${num}`).value = exp.model;\n updateStrengths(num);\n });\n\n // Apply chairman config\n document.getElementById('providerChairman').value = preset.chairman.provider;\n onProviderChange('Chairman');\n document.getElementById('modelChairman').value = preset.chairman.model;\n updateStrengths('Chairman');\n\n updateEstimate();\n }\n\n // ============================================\n // Estimation\n // ============================================\n function updateEstimate() {\n let totalCost = 0;\n let fastestTime = true;\n\n [1, 2, 3, 'Chairman'].forEach(num => {\n const provider = document.getElementById(`provider${num}`).value;\n const modelId = document.getElementById(`model${num}`).value;\n const model = PROVIDERS[provider]?.models.find(m => m.id === modelId);\n \n if (model) {\n totalCost += model.cost;\n if (provider !== 'groq') fastestTime = false;\n }\n });\n\n const enableReview = document.getElementById('enableReview').checked;\n if (enableReview) {\n totalCost *= 1.5;\n }\n\n // Update UI\n document.getElementById('costEstimate').textContent = totalCost === 0 ? 'Gratuit!' : `~${Math.round(totalCost)} DZD`;\n document.getElementById('timeEstimate').textContent = fastestTime ? '~5s' : (enableReview ? '~30s' : '~15s');\n \n // Show warning for expensive configs\n document.getElementById('warningBox').style.display = totalCost > 100 ? 'block' : 'none';\n }\n\n // ============================================\n // Execute Council\n // ============================================\n async function executeCouncil() {\n const query = document.getElementById('queryInput').value.trim();\n if (!query) {\n showError('Veuillez entrer une question');\n return;\n }\n\n // Get configuration\n const experts = [1, 2, 3].map(num => ({\n provider: document.getElementById(`provider${num}`).value,\n model: document.getElementById(`model${num}`).value\n }));\n \n const chairman = {\n provider: document.getElementById('providerChairman').value,\n model: document.getElementById('modelChairman').value\n };\n\n const enableReview = document.getElementById('enableReview').checked;\n\n // Show loading\n document.getElementById('loadingBox').style.display = 'block';\n document.getElementById('resultsSection').style.display = 'none';\n document.getElementById('submitBtn').disabled = true;\n\n const startTime = Date.now();\n\n try {\n // Call each expert in parallel\n document.getElementById('loadingStatus').textContent = 'Les experts analysent votre question...';\n \n const expertPromises = experts.map((exp, idx) => \n callLLM(exp.model, query, `Expert ${idx + 1}`)\n );\n\n const expertResponses = await Promise.all(expertPromises);\n\n // Store opinions\n currentOpinions = {};\n expertResponses.forEach((response, idx) => {\n currentOpinions[`expert${idx + 1}`] = response;\n });\n\n // Chairman synthesis\n document.getElementById('loadingStatus').textContent = 'Le Chairman synthétise les réponses...';\n \n const synthesisPrompt = `Tu es le Chairman d'un conseil d'experts IA. \nVoici la question posée: \"${query}\"\n\nVoici les opinions des experts:\n\nExpert 1 (${getModelName(experts[0].model)}):\n${expertResponses[0]}\n\nExpert 2 (${getModelName(experts[1].model)}):\n${expertResponses[1]}\n\nExpert 3 (${getModelName(experts[2].model)}):\n${expertResponses[2]}\n\nEn tant que Chairman, fais une synthèse complète qui:\n1. Identifie les points de consensus\n2. Note les perspectives uniques de chaque expert\n3. Propose une réponse finale équilibrée et complète\n\nRéponds en français de manière structurée.`;\n\n const finalResponse = await callLLM(chairman.model, synthesisPrompt, 'Chairman');\n\n // Calculate stats\n const execTime = ((Date.now() - startTime) / 1000).toFixed(1);\n\n // Display results\n displayResults({\n finalResponse,\n chairman,\n experts,\n execTime,\n opinions: currentOpinions\n });\n\n } catch (error) {\n showError(`Erreur: ${error.message}`);\n } finally {\n document.getElementById('loadingBox').style.display = 'none';\n document.getElementById('submitBtn').disabled = false;\n }\n }\n\n async function callLLM(model, prompt, role) {\n try {\n // Check if it's an Ollama model\n if (model.startsWith('ollama.')) {\n return await callOllama(model, prompt, role);\n }\n\n // Check if it's an OpenRouter model\n if (model.startsWith('openrouter.')) {\n return await callOpenRouter(model, prompt, role);\n }\n\n // Check if it's a Groq model - call directly\n if (model.startsWith('groq.')) {\n return await callGroq(model, prompt, role);\n }\n\n // Check if it's a DeepSeek model - call directly\n if (model.startsWith('deepseek.')) {\n return await callDeepSeek(model, prompt, role);\n }\n\n // Standard API call for other providers (fallback)\n const response = await fetch('/api/llm/chat', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n model: model,\n messages: [{ role: 'user', content: prompt }],\n temperature: 0.7,\n max_tokens: 2000\n })\n });\n\n const data = await response.json();\n \n if (!response.ok || !data.success) {\n let errorMsg = 'Erreur inconnue';\n if (typeof data.detail === 'string') {\n errorMsg = data.detail;\n } else if (typeof data.error === 'string') {\n errorMsg = data.error;\n } else if (data.detail && typeof data.detail === 'object') {\n errorMsg = JSON.stringify(data.detail);\n } else if (data.error && typeof data.error === 'object') {\n errorMsg = JSON.stringify(data.error);\n }\n throw new Error(`${role}: ${errorMsg}`);\n }\n\n return data.answer || data.response || 'Réponse vide';\n } catch (fetchError) {\n if (fetchError.message.includes(role)) {\n throw fetchError;\n }\n throw new Error(`${role}: ${fetchError.message}`);\n }\n }\n\n async function callOllama(model, prompt, role) {\n // Extract actual model name (e.g., \"ollama.mistral:7b\" -> \"mistral:7b\")\n const ollamaModel = model.replace('ollama.', '');\n const systemPrompt = 'You MUST respond in the same language as the user input. Match the language exactly.';\n \n try {\n const response = await fetch('/ollama/api/generate', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n model: ollamaModel,\n prompt: prompt,\n system: systemPrompt,\n stream: false\n })\n });\n\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}`);\n }\n\n const data = await response.json();\n return data.response || 'Réponse vide';\n } catch (error) {\n throw new Error(`${role} (Ollama): ${error.message}`);\n }\n }\n\n async function callOpenRouter(model, prompt, role) {\n // Extract actual model name (e.g., \"openrouter.deepseek/deepseek-chat\" -> \"deepseek/deepseek-chat\")\n const openrouterModel = model.replace('openrouter.', '');\n \n try {\n const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${OPENROUTER_API_KEY}`,\n 'HTTP-Referer': window.location.href,\n 'X-Title': 'IAFactory Council'\n },\n body: JSON.stringify({\n model: openrouterModel,\n messages: [\n { role: 'system', content: 'You MUST respond in the same language as the user input. Match the language exactly.' },\n { role: 'user', content: prompt }\n ],\n max_tokens: 2000,\n temperature: 0.7\n })\n });\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n const errorMsg = errorData.error?.message || `HTTP ${response.status}`;\n throw new Error(errorMsg);\n }\n\n const data = await response.json();\n return data.choices?.[0]?.message?.content || 'Réponse vide';\n } catch (error) {\n throw new Error(`${role} (OpenRouter): ${error.message}`);\n }\n }\n\n async function callGroq(model, prompt, role) {\n // Extract actual model name (e.g., \"groq.llama-3.3-70b-versatile\" -> \"llama-3.3-70b-versatile\")\n const groqModel = model.replace('groq.', '');\n \n try {\n const response = await fetch('https://api.groq.com/openai/v1/chat/completions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${GROQ_API_KEY}`\n },\n body: JSON.stringify({\n model: groqModel,\n messages: [\n { role: 'system', content: 'You MUST respond in the same language as the user input. Match the language exactly.' },\n { role: 'user', content: prompt }\n ],\n max_tokens: 2000,\n temperature: 0.7\n })\n });\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n const errorMsg = errorData.error?.message || `HTTP ${response.status}`;\n throw new Error(errorMsg);\n }\n\n const data = await response.json();\n return data.choices?.[0]?.message?.content || 'Réponse vide';\n } catch (error) {\n throw new Error(`${role} (Groq): ${error.message}`);\n }\n }\n\n async function callDeepSeek(model, prompt, role) {\n // Extract actual model name (e.g., \"deepseek.deepseek-chat\" -> \"deepseek-chat\")\n const deepseekModel = model.replace('deepseek.', '');\n \n try {\n const response = await fetch('https://api.deepseek.com/v1/chat/completions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${DEEPSEEK_API_KEY}`\n },\n body: JSON.stringify({\n model: deepseekModel,\n messages: [\n { role: 'system', content: 'You MUST respond in the same language as the user input. If the user writes in French, respond in French. If in English, respond in English. Never respond in Chinese unless the user writes in Chinese.' },\n { role: 'user', content: prompt }\n ],\n max_tokens: 2000,\n temperature: 0.7\n })\n });\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n const errorMsg = errorData.error?.message || `HTTP ${response.status}`;\n throw new Error(errorMsg);\n }\n\n const data = await response.json();\n return data.choices?.[0]?.message?.content || 'Réponse vide';\n } catch (error) {\n throw new Error(`${role} (DeepSeek): ${error.message}`);\n }\n }\n\n function getModelName(modelId) {\n for (const provider of Object.values(PROVIDERS)) {\n const model = provider.models.find(m => m.id === modelId);\n if (model) return model.name;\n }\n return modelId;\n }\n\n // ============================================\n // Display Results\n // ============================================\n function displayResults(data) {\n document.getElementById('resultsSection').style.display = 'block';\n document.getElementById('finalResponse').textContent = data.finalResponse;\n document.getElementById('chairmanName').textContent = `(${getModelName(data.chairman.model)})`;\n document.getElementById('execTime').innerHTML = `<i class=\"fas fa-clock\"></i> ${data.execTime}s`;\n document.getElementById('actualCost').innerHTML = `<i class=\"fas fa-coins\"></i> Estimé`;\n document.getElementById('expertsCount').innerHTML = `<i class=\"fas fa-users\"></i> 3 experts + Chairman`;\n\n // Build opinion tabs\n const tabsHTML = data.experts.map((exp, idx) => `\n <button class=\"opinion-tab ${idx === 0 ? 'active' : ''}\" \n onclick=\"showOpinion('expert${idx + 1}', this)\">\n Expert ${idx + 1} (${getModelName(exp.model)})\n </button>\n `).join('');\n\n document.getElementById('opinionTabs').innerHTML = tabsHTML;\n showOpinion('expert1', document.querySelector('.opinion-tab'));\n\n // Scroll to results\n document.getElementById('resultsSection').scrollIntoView({ behavior: 'smooth' });\n }\n\n function showOpinion(expertKey, tabElement) {\n // Update tabs\n document.querySelectorAll('.opinion-tab').forEach(tab => tab.classList.remove('active'));\n tabElement.classList.add('active');\n\n // Show content\n document.getElementById('opinionContent').textContent = currentOpinions[expertKey] || 'Opinion non disponible';\n }\n\n // ============================================\n // Error Handling\n // ============================================\n function showError(message) {\n const toast = document.getElementById('errorToast');\n document.getElementById('errorMessage').textContent = message;\n toast.classList.add('visible');\n\n setTimeout(() => {\n toast.classList.remove('visible');\n }, 5000);\n }\n\n // Listen for review checkbox\n document.getElementById('enableReview').addEventListener('change', updateEstimate);\n </script>\n</body>\n</html>\n"}, "6e3e559c95f7add9ca35fe4046b933a73195120f": {"t": 1785749290.6773176, "n": 58274, "h": "f1d4d1fa1eed03d7", "c": "<!DOCTYPE html>\n<html lang=\"fr\" data-theme=\"dark\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Build Your Council - IA Factory Algeria</title>\n <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n <link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap\" rel=\"stylesheet\">\n <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css\">\n <style>\n :root {\n /* Dark Mode (default) - IAFactory Theme */\n --bg-primary: #020617;\n --bg-secondary: #0a0f1a;\n --bg-card: #0f1629;\n --bg-card-hover: #151d30;\n --border-color: rgba(255, 255, 255, 0.12);\n --text-primary: #f8fafc;\n --text-secondary: rgba(248, 250, 252, 0.75);\n --text-muted: rgba(248, 250, 252, 0.5);\n --accent-primary: #00a651;\n --accent-secondary: #00c95d;\n --accent-gradient: linear-gradient(135deg, #00a651, #008c45);\n --success: #10b981;\n --success-bg: rgba(16, 185, 129, 0.1);\n --warning: #f59e0b;\n --warning-bg: rgba(245, 158, 11, 0.1);\n --error: #ef4444;\n --input-bg: #0a0f1a;\n --input-border: rgba(255, 255, 255, 0.12);\n --input-text: #f8fafc;\n --input-placeholder: rgba(248, 250, 252, 0.5);\n }\n\n [data-theme=\"light\"] {\n --bg-primary: #f7f5f0;\n --bg-secondary: #efe9df;\n --bg-card: #ffffff;\n --bg-card-hover: #f7f5f0;\n --border-color: rgba(0, 0, 0, 0.08);\n --text-primary: #0f172a;\n --text-secondary: rgba(15, 23, 42, 0.7);\n --text-muted: rgba(15, 23, 42, 0.5);\n --accent-primary: #00a651;\n --accent-secondary: #008c45;\n --accent-gradient: linear-gradient(135deg, #00a651, #008c45);\n --success-bg: rgba(16, 185, 129, 0.1);\n --warning-bg: rgba(245, 158, 11, 0.1);\n --input-bg: #ffffff;\n --input-border: rgba(0, 0, 0, 0.12);\n --input-text: #0f172a;\n --input-placeholder: rgba(15, 23, 42, 0.5);\n }\n\n * {\n font-family: 'Inter', sans-serif;\n margin: 0;\n padding: 0;\n box-sizing: border-box;\n }\n\n body {\n background: var(--bg-primary);\n color: var(--text-primary);\n min-height: 100vh;\n transition: background 0.3s ease, color 0.3s ease;\n }\n\n /* Header */\n .main-header {\n background: var(--bg-secondary);\n border-bottom: 1px solid var(--border-color);\n padding: 1rem 2rem;\n display: flex;\n justify-content: space-between;\n align-items: center;\n position: sticky;\n top: 0;\n z-index: 100;\n }\n\n .logo {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n }\n\n .logo-icon {\n width: 40px;\n height: 40px;\n background: var(--accent-gradient);\n border-radius: 10px;\n display: flex;\n align-items: center;\n justify-content: center;\n color: white;\n font-size: 1.25rem;\n }\n\n .logo h1 {\n font-size: 1.25rem;\n font-weight: 700;\n background: var(--accent-gradient);\n -webkit-background-clip: text;\n -webkit-text-fill-color: transparent;\n }\n\n .header-actions {\n display: flex;\n align-items: center;\n gap: 1rem;\n }\n\n .theme-toggle {\n background: var(--bg-card);\n border: 1px solid var(--border-color);\n color: var(--text-primary);\n padding: 0.5rem 0.75rem;\n border-radius: 8px;\n cursor: pointer;\n font-size: 1rem;\n transition: all 0.2s;\n }\n\n .theme-toggle:hover {\n background: var(--bg-card-hover);\n border-color: var(--accent-primary);\n }\n\n .home-btn {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n padding: 0.5rem 1rem;\n background: var(--accent-gradient);\n color: white;\n border: none;\n border-radius: 8px;\n font-size: 0.875rem;\n font-weight: 500;\n cursor: pointer;\n text-decoration: none;\n transition: all 0.2s;\n }\n\n .home-btn:hover {\n transform: translateY(-1px);\n box-shadow: 0 4px 12px rgba(124, 58, 237, 0.3);\n }\n\n /* Main Container */\n .container {\n max-width: 1400px;\n margin: 0 auto;\n padding: 2rem;\n }\n\n /* Page Header */\n .page-header {\n text-align: center;\n margin-bottom: 2rem;\n }\n\n .page-header h1 {\n font-size: 2.5rem;\n font-weight: 800;\n margin-bottom: 0.5rem;\n background: var(--accent-gradient);\n -webkit-background-clip: text;\n -webkit-text-fill-color: transparent;\n }\n\n .page-header p {\n font-size: 1.1rem;\n color: var(--text-secondary);\n }\n\n /* Presets */\n .presets-grid {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));\n gap: 1rem;\n margin: 1.5rem 0;\n }\n\n .preset-card {\n padding: 1.25rem;\n background: var(--bg-card);\n border: 2px solid var(--border-color);\n border-radius: 12px;\n cursor: pointer;\n transition: all 0.2s;\n text-align: center;\n }\n\n .preset-card:hover {\n transform: translateY(-2px);\n border-color: var(--accent-primary);\n box-shadow: 0 4px 12px rgba(124, 58, 237, 0.15);\n }\n\n .preset-card.active {\n border-color: var(--accent-primary);\n background: rgba(124, 58, 237, 0.1);\n }\n\n .preset-icon {\n font-size: 2rem;\n margin-bottom: 0.5rem;\n }\n\n .preset-name {\n font-weight: 600;\n margin-bottom: 0.25rem;\n color: var(--text-primary);\n }\n\n .preset-desc {\n font-size: 0.75rem;\n color: var(--text-secondary);\n }\n\n /* Config Sections */\n .config-section {\n background: var(--bg-card);\n border: 1px solid var(--border-color);\n border-radius: 16px;\n padding: 1.5rem;\n margin-bottom: 1.5rem;\n }\n\n .section-title {\n font-size: 1.125rem;\n font-weight: 700;\n margin-bottom: 1.5rem;\n display: flex;\n align-items: center;\n gap: 0.5rem;\n color: var(--text-primary);\n }\n\n /* Experts Grid */\n .experts-grid {\n display: grid;\n grid-template-columns: repeat(4, 1fr);\n gap: 1.5rem;\n }\n\n @media (max-width: 1200px) {\n .experts-grid {\n grid-template-columns: repeat(2, 1fr);\n }\n }\n\n @media (max-width: 600px) {\n .experts-grid {\n grid-template-columns: 1fr;\n }\n }\n\n .expert-card {\n background: var(--bg-secondary);\n border: 2px solid var(--border-color);\n border-radius: 12px;\n padding: 1.25rem;\n transition: all 0.2s;\n }\n\n .expert-card:hover {\n border-color: var(--accent-secondary);\n }\n\n .expert-card.expert-1 { border-top: 3px solid var(--accent-primary); }\n .expert-card.expert-2 { border-top: 3px solid var(--accent-primary); }\n .expert-card.expert-3 { border-top: 3px solid var(--accent-primary); }\n .expert-card.chairman { border-top: 3px solid var(--accent-secondary); }\n\n .expert-label {\n font-weight: 700;\n margin-bottom: 1rem;\n font-size: 0.95rem;\n display: flex;\n align-items: center;\n gap: 0.5rem;\n }\n\n .expert-1 .expert-label { color: var(--accent-primary); }\n .expert-2 .expert-label { color: var(--accent-primary); }\n .expert-3 .expert-label { color: var(--accent-primary); }\n .chairman .expert-label { color: var(--accent-secondary); }\n\n .form-group {\n margin-bottom: 1rem;\n }\n\n .form-group:last-child {\n margin-bottom: 0;\n }\n\n .form-group label {\n display: block;\n font-size: 0.75rem;\n font-weight: 600;\n color: var(--text-secondary);\n margin-bottom: 0.4rem;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n }\n\n .form-group select {\n width: 100%;\n padding: 0.75rem 1rem;\n background: var(--input-bg);\n border: 1px solid var(--input-border);\n border-radius: 8px;\n font-size: 0.875rem;\n color: var(--input-text);\n cursor: pointer;\n transition: all 0.2s;\n appearance: none;\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%2394a3b8'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E\");\n background-repeat: no-repeat;\n background-position: right 0.75rem center;\n background-size: 1rem;\n padding-right: 2.5rem;\n }\n\n .form-group select:focus {\n outline: none;\n border-color: var(--accent-primary);\n box-shadow: 0 0 0 3px rgba(124, 58, 237, 0.15);\n }\n\n .form-group select option {\n background: var(--bg-card);\n color: var(--text-primary);\n padding: 0.5rem;\n }\n\n .strengths-box {\n font-size: 0.8rem;\n color: var(--text-secondary);\n padding: 0.5rem 0.75rem;\n background: rgba(0, 166, 81, 0.1);\n border-radius: 6px;\n margin-top: 0.75rem;\n }\n\n /* Estimate Box */\n .estimate-box {\n background: var(--accent-gradient);\n color: white;\n padding: 1.5rem;\n border-radius: 12px;\n margin-bottom: 1.5rem;\n }\n\n .estimate-title {\n font-size: 1.1rem;\n font-weight: 700;\n margin-bottom: 1rem;\n }\n\n .estimate-grid {\n display: grid;\n grid-template-columns: repeat(3, 1fr);\n gap: 1rem;\n }\n\n @media (max-width: 600px) {\n .estimate-grid {\n grid-template-columns: 1fr;\n }\n }\n\n .estimate-item {\n background: rgba(255,255,255,0.15);\n padding: 1rem;\n border-radius: 8px;\n text-align: center;\n }\n\n .estimate-label {\n font-size: 0.85rem;\n opacity: 0.9;\n }\n\n .estimate-value {\n font-size: 1.5rem;\n font-weight: 700;\n margin-top: 0.25rem;\n }\n\n .warning-box {\n display: none;\n margin-top: 1rem;\n padding: 0.75rem;\n background: rgba(245, 158, 11, 0.2);\n border-radius: 8px;\n font-size: 0.875rem;\n }\n\n /* Query Section */\n .query-section textarea {\n width: 100%;\n min-height: 150px;\n padding: 1rem;\n background: var(--input-bg);\n border: 2px solid var(--input-border);\n border-radius: 12px;\n font-size: 0.95rem;\n color: var(--input-text);\n resize: vertical;\n transition: all 0.2s;\n line-height: 1.6;\n }\n\n .query-section textarea::placeholder {\n color: var(--input-placeholder);\n }\n\n .query-section textarea:focus {\n outline: none;\n border-color: var(--accent-primary);\n box-shadow: 0 0 0 3px rgba(124, 58, 237, 0.15);\n }\n\n /* Options Row */\n .options-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n margin-top: 1rem;\n flex-wrap: wrap;\n gap: 1rem;\n }\n\n .checkbox-label {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n cursor: pointer;\n font-size: 0.9rem;\n color: var(--text-secondary);\n }\n\n .checkbox-label input[type=\"checkbox\"] {\n width: 1.25rem;\n height: 1.25rem;\n cursor: pointer;\n accent-color: var(--accent-primary);\n }\n\n /* Submit Button */\n .btn-submit {\n padding: 1rem 2rem;\n background: var(--accent-gradient);\n color: white;\n border: none;\n border-radius: 10px;\n font-size: 1rem;\n font-weight: 600;\n cursor: pointer;\n transition: all 0.2s;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 0.5rem;\n }\n\n .btn-submit:hover:not(:disabled) {\n transform: translateY(-1px);\n box-shadow: 0 6px 20px rgba(124, 58, 237, 0.35);\n }\n\n .btn-submit:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n }\n\n /* Loading */\n .loading-box {\n display: none;\n padding: 1.5rem;\n background: var(--warning-bg);\n border-left: 4px solid var(--warning);\n border-radius: 8px;\n margin-bottom: 1.5rem;\n }\n\n .loading-content {\n display: flex;\n align-items: center;\n gap: 1rem;\n }\n\n .spinner {\n border: 3px solid rgba(245, 158, 11, 0.3);\n border-top-color: var(--warning);\n border-radius: 50%;\n width: 24px;\n height: 24px;\n animation: spin 0.8s linear infinite;\n }\n\n @keyframes spin {\n to { transform: rotate(360deg); }\n }\n\n /* Results Section */\n .results-section {\n display: none;\n }\n\n .final-response-box {\n background: linear-gradient(135deg, rgba(16, 185, 129, 0.15), rgba(5, 150, 105, 0.1));\n border: 1px solid rgba(16, 185, 129, 0.3);\n border-left: 4px solid var(--success);\n border-radius: 16px;\n padding: 2rem;\n margin-bottom: 1.5rem;\n }\n\n .result-header {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n margin-bottom: 1rem;\n font-size: 1.25rem;\n font-weight: 700;\n color: var(--success);\n }\n\n .result-text {\n font-size: 1rem;\n line-height: 1.8;\n color: var(--text-primary);\n white-space: pre-wrap;\n }\n\n .result-meta {\n margin-top: 1.5rem;\n padding-top: 1rem;\n border-top: 1px solid rgba(16, 185, 129, 0.2);\n display: flex;\n gap: 2rem;\n font-size: 0.875rem;\n color: var(--text-secondary);\n }\n\n /* Opinions Section */\n .opinions-box {\n background: var(--bg-card);\n border: 1px solid var(--border-color);\n border-radius: 16px;\n padding: 1.5rem;\n }\n\n .opinion-tabs {\n display: flex;\n gap: 0.5rem;\n margin-bottom: 1.5rem;\n border-bottom: 2px solid var(--border-color);\n padding-bottom: 0.5rem;\n overflow-x: auto;\n }\n\n .opinion-tab {\n padding: 0.75rem 1.25rem;\n background: transparent;\n border: none;\n border-bottom: 3px solid transparent;\n cursor: pointer;\n font-weight: 500;\n color: var(--text-secondary);\n transition: all 0.2s;\n white-space: nowrap;\n }\n\n .opinion-tab:hover {\n color: var(--text-primary);\n }\n\n .opinion-tab.active {\n color: var(--accent-primary);\n border-bottom-color: var(--accent-primary);\n }\n\n .opinion-content {\n padding: 1.5rem;\n background: var(--bg-secondary);\n border-radius: 12px;\n line-height: 1.8;\n white-space: pre-wrap;\n color: var(--text-primary);\n max-height: 400px;\n overflow-y: auto;\n }\n\n /* Error Toast */\n .error-toast {\n position: fixed;\n bottom: 2rem;\n right: 2rem;\n padding: 1rem 1.5rem;\n background: var(--error);\n color: white;\n border-radius: 10px;\n font-size: 0.875rem;\n display: none;\n z-index: 1000;\n animation: slideIn 0.3s ease;\n max-width: 400px;\n }\n\n .error-toast.visible {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n }\n\n @keyframes slideIn {\n from {\n transform: translateY(20px);\n opacity: 0;\n }\n to {\n transform: translateY(0);\n opacity: 1;\n }\n }\n\n /* Scrollbar */\n ::-webkit-scrollbar {\n width: 8px;\n height: 8px;\n }\n\n ::-webkit-scrollbar-track {\n background: transparent;\n }\n\n ::-webkit-scrollbar-thumb {\n background: var(--border-color);\n border-radius: 4px;\n }\n\n ::-webkit-scrollbar-thumb:hover {\n background: var(--text-muted);\n }\n </style>\n\n<!-- IAFactory i18n System - TRILINGUE -->\n<link rel=\"stylesheet\" href=\"/shared/iafactory-theme.css\">\n<script src=\"/shared/i18n.js\"></script>\n<script src=\"/shared/language-switcher.js\"></script>\n\n</head>\n<body>\n <!-- Header -->\n <header class=\"main-header\">\n <div class=\"logo\">\n <div class=\"logo-icon\">\n <i class=\"fas fa-users\"></i>\n </div>\n <h1>Council IA</h1>\n </div>\n <div class=\"header-actions\">\n\n<!-- Language Switcher -->\n<div data-language-switcher></div>\n\n <button class=\"theme-toggle\" onclick=\"toggleTheme()\" title=\"Changer le thème\">\n <i class=\"fas fa-moon\" id=\"theme-icon\"></i>\n </button>\n <a href=\"/\" class=\"home-btn\">\n <i class=\"fas fa-home\"></i>\n Accueil\n </a>\n </div>\n </header>\n\n <div class=\"container\">\n <!-- Page Header -->\n <div class=\"page-header\">\n <h1>🎯 Build Your Council</h1>\n <p>Composez votre équipe d'experts IA sur mesure avec une infinité de combinaisons</p>\n </div>\n\n <!-- Presets -->\n <div class=\"config-section\">\n <h2 class=\"section-title\">✨ Configurations recommandées (Groq ⚡ + OpenRouter 🌐)</h2>\n <div class=\"presets-grid\" id=\"presetsGrid\">\n <div class=\"preset-card\" onclick=\"applyPreset('fast')\" style=\"border: 2px solid var(--accent-primary);\">\n <div class=\"preset-icon\">⚡</div>\n <div class=\"preset-name\">Groq Diversifié ⭐</div>\n <div class=\"preset-desc\">Llama 70B + Qwen + Kimi</div>\n </div>\n <div class=\"preset-card\" onclick=\"applyPreset('deepseek')\" style=\"border: 2px solid var(--accent-primary);\">\n <div class=\"preset-icon\">🇨🇳</div>\n <div class=\"preset-name\">DeepSeek Power ⭐</div>\n <div class=\"preset-desc\">Chat + R1 Reasoner</div>\n </div>\n <div class=\"preset-card\" onclick=\"applyPreset('diversified')\">\n <div class=\"preset-icon\">🎯</div>\n <div class=\"preset-name\">Mix Groq+DeepSeek</div>\n <div class=\"preset-desc\">Llama + Kimi + DeepSeek</div>\n </div>\n <div class=\"preset-card\" onclick=\"applyPreset('llama4')\">\n <div class=\"preset-icon\">🦙</div>\n <div class=\"preset-name\">Llama 4 Meta</div>\n <div class=\"preset-desc\">Maverick + Scout + 70B</div>\n </div>\n <div class=\"preset-card\" onclick=\"applyPreset('openrouter')\">\n <div class=\"preset-icon\">🌐</div>\n <div class=\"preset-name\">OpenRouter FREE</div>\n <div class=\"preset-desc\">Gemini + Llama + Mistral</div>\n </div>\n <div class=\"preset-card\" onclick=\"applyPreset('hybrid')\">\n <div class=\"preset-icon\">🔀</div>\n <div class=\"preset-name\">Hybride</div>\n <div class=\"preset-desc\">Groq + Gemini + Qwen</div>\n </div>\n <div class=\"preset-card\" onclick=\"applyPreset('local')\">\n <div class=\"preset-icon\">🏠</div>\n <div class=\"preset-name\">100% Local</div>\n <div class=\"preset-desc\">Ollama (lent sur VPS)</div>\n </div>\n </div>\n </div>\n\n <!-- Configuration -->\n <div class=\"config-section\">\n <h2 class=\"section-title\">⚙️ Configuration du Council</h2>\n \n <div class=\"experts-grid\">\n <!-- Expert 1 -->\n <div class=\"expert-card expert-1\">\n <div class=\"expert-label\">\n <i class=\"fas fa-user-tie\"></i> Expert 1\n </div>\n <div class=\"form-group\">\n <label>Provider</label>\n <select id=\"provider1\" onchange=\"onProviderChange(1)\">\n <option value=\"deepseek\">🇨🇳 DeepSeek (Économique)</option>\n <option value=\"groq\" selected>⚡ Groq (Ultra-rapide)</option>\n <option value=\"openrouter\">🌐 OpenRouter</option>\n <option value=\"ollama\">🏠 Ollama (Local - lent)</option>\n </select>\n </div>\n <div class=\"form-group\">\n <label>Modèle</label>\n <select id=\"model1\"></select>\n </div>\n <div class=\"strengths-box\" id=\"strengths1\">💪 Sélectionnez un modèle</div>\n </div>\n\n <!-- Expert 2 -->\n <div class=\"expert-card expert-2\">\n <div class=\"expert-label\">\n <i class=\"fas fa-user-tie\"></i> Expert 2\n </div>\n <div class=\"form-group\">\n <label>Provider</label>\n <select id=\"provider2\" onchange=\"onProviderChange(2)\">\n <option value=\"deepseek\">🇨🇳 DeepSeek (Économique)</option>\n <option value=\"groq\" selected>⚡ Groq (Ultra-rapide)</option>\n <option value=\"openrouter\">🌐 OpenRouter</option>\n <option value=\"ollama\">🏠 Ollama (Local - lent)</option>\n </select>\n </div>\n <div class=\"form-group\">\n <label>Modèle</label>\n <select id=\"model2\"></select>\n </div>\n <div class=\"strengths-box\" id=\"strengths2\">💪 Sélectionnez un modèle</div>\n </div>\n\n <!-- Expert 3 -->\n <div class=\"expert-card expert-3\">\n <div class=\"expert-label\">\n <i class=\"fas fa-user-tie\"></i> Expert 3\n </div>\n <div class=\"form-group\">\n <label>Provider</label>\n <select id=\"provider3\" onchange=\"onProviderChange(3)\">\n <option value=\"deepseek\">🇨🇳 DeepSeek (Économique)</option>\n <option value=\"groq\" selected>⚡ Groq (Ultra-rapide)</option>\n <option value=\"openrouter\">🌐 OpenRouter</option>\n <option value=\"ollama\">🏠 Ollama (Local - lent)</option>\n </select>\n </div>\n <div class=\"form-group\">\n <label>Modèle</label>\n <select id=\"model3\"></select>\n </div>\n <div class=\"strengths-box\" id=\"strengths3\">💪 Sélectionnez un modèle</div>\n </div>\n\n <!-- Chairman -->\n <div class=\"expert-card chairman\">\n <div class=\"expert-label\">\n <i class=\"fas fa-crown\"></i> Chairman (Synthèse)\n </div>\n <div class=\"form-group\">\n <label>Provider</label>\n <select id=\"providerChairman\" onchange=\"onProviderChange('Chairman')\">\n <option value=\"deepseek\">🇨🇳 DeepSeek (Économique)</option>\n <option value=\"groq\" selected>⚡ Groq (Ultra-rapide)</option>\n <option value=\"openrouter\">🌐 OpenRouter</option>\n <option value=\"ollama\">🏠 Ollama (Local - lent)</option>\n </select>\n </div>\n <div class=\"form-group\">\n <label>Modèle</label>\n <select id=\"modelChairman\"></select>\n </div>\n <div class=\"strengths-box\" id=\"strengthsChairman\">💪 Sélectionnez un modèle</div>\n </div>\n </div>\n </div>\n\n <!-- Estimation -->\n <div class=\"estimate-box\">\n <div class=\"estimate-title\">📊 Estimation</div>\n <div class=\"estimate-grid\">\n <div class=\"estimate-item\">\n <div class=\"estimate-label\">💰 Coût estimé</div>\n <div class=\"estimate-value\" id=\"costEstimate\">~50 DZD</div>\n </div>\n <div class=\"estimate-item\">\n <div class=\"estimate-label\">⏱️ Temps estimé</div>\n <div class=\"estimate-value\" id=\"timeEstimate\">~15s</div>\n </div>\n <div class=\"estimate-item\">\n <div class=\"estimate-label\">🎯 Combinaisons</div>\n <div class=\"estimate-value\">∞</div>\n </div>\n </div>\n <div class=\"warning-box\" id=\"warningBox\">\n ⚠️ Configuration premium : coût élevé. Utilisez Groq pour réduire les coûts.\n </div>\n </div>\n\n <!-- Query Section -->\n <div class=\"config-section query-section\">\n <h2 class=\"section-title\">💬 Votre question</h2>\n <textarea \n id=\"queryInput\" \n placeholder=\"Posez votre question au Council personnalisé...\n\nExemples:\n• Quelles sont les meilleures pratiques pour sécuriser une API REST ?\n• Comment optimiser PostgreSQL pour des millions de lignes ?\n• Expliquez la différence entre authentification et autorisation\n• Compare les architectures microservices vs monolithique\"\n ></textarea>\n \n <div class=\"options-row\">\n <label class=\"checkbox-label\">\n <input type=\"checkbox\" id=\"enableReview\">\n <span>Activer la revue croisée (+50% temps, meilleure qualité)</span>\n </label>\n <button class=\"btn-submit\" id=\"submitBtn\" onclick=\"executeCouncil()\">\n <i class=\"fas fa-bolt\"></i>\n Lancer le Council\n </button>\n </div>\n </div>\n\n <!-- Loading -->\n <div class=\"loading-box\" id=\"loadingBox\">\n <div class=\"loading-content\">\n <div class=\"spinner\"></div>\n <div>\n <div style=\"font-weight: 600; margin-bottom: 0.25rem;\">Council en délibération...</div>\n <div style=\"font-size: 0.875rem; color: var(--text-secondary);\" id=\"loadingStatus\">Les experts analysent votre question</div>\n </div>\n </div>\n </div>\n\n <!-- Results -->\n <div class=\"results-section\" id=\"resultsSection\">\n <div class=\"final-response-box\">\n <div class=\"result-header\">\n <i class=\"fas fa-crown\"></i>\n <span>Réponse finale du Chairman</span>\n <span id=\"chairmanName\" style=\"font-size: 0.9rem; font-weight: normal; color: var(--text-secondary);\"></span>\n </div>\n <div class=\"result-text\" id=\"finalResponse\"></div>\n <div class=\"result-meta\">\n <span id=\"execTime\"><i class=\"fas fa-clock\"></i> -</span>\n <span id=\"actualCost\"><i class=\"fas fa-coins\"></i> -</span>\n <span id=\"expertsCount\"><i class=\"fas fa-users\"></i> -</span>\n </div>\n </div>\n\n <div class=\"opinions-box\">\n <h3 class=\"section-title\">🎯 Opinions individuelles des experts</h3>\n <div class=\"opinion-tabs\" id=\"opinionTabs\"></div>\n <div class=\"opinion-content\" id=\"opinionContent\">Sélectionnez un expert pour voir son opinion</div>\n </div>\n </div>\n </div>\n\n <!-- Error Toast -->\n <div class=\"error-toast\" id=\"errorToast\">\n <i class=\"fas fa-exclamation-circle\"></i>\n <span id=\"errorMessage\"></span>\n </div>\n\n <script>\n // ============================================\n // Provider and Model Configuration\n // ============================================\n // API Keys pour appels directs\n const OPENROUTER_API_KEY = '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';\n const GROQ_API_KEY = 'YOUR_GROQ_API_KEY';\n const DEEPSEEK_API_KEY = 'sk-e2d7d214600946479856ffafbe1ce392';\n\n const PROVIDERS = {\n deepseek: {\n name: 'DeepSeek 🇨🇳',\n models: [\n { id: 'deepseek.deepseek-chat', name: 'DeepSeek Chat V3', strengths: ['Économique', 'Raisonnement', 'Code'], cost: 1 },\n { id: 'deepseek.deepseek-reasoner', name: 'DeepSeek R1 🧠', strengths: ['Raisonnement avancé', 'Math', 'Logique'], cost: 3 }\n ]\n },\n groq: {\n name: 'Groq ⚡',\n models: [\n { id: 'groq.llama-3.3-70b-versatile', name: 'Llama 3.3 70B', strengths: ['Puissant', 'Versatile', '131K ctx'], cost: 0 },\n { id: 'groq.llama-3.1-8b-instant', name: 'Llama 3.1 8B', strengths: ['Ultra-rapide', 'Gratuit'], cost: 0 },\n { id: 'groq.moonshotai/kimi-k2-instruct', name: 'Kimi K2 🌙', strengths: ['Moonshot AI', 'Raisonnement'], cost: 0 },\n { id: 'groq.qwen/qwen3-32b', name: 'Qwen3 32B', strengths: ['Alibaba', 'Multilingue', 'Code'], cost: 0 },\n { id: 'groq.meta-llama/llama-4-maverick-17b-128e-instruct', name: 'Llama 4 Maverick 🦙', strengths: ['Meta', 'Nouveau', '131K ctx'], cost: 0 },\n { id: 'groq.meta-llama/llama-4-scout-17b-16e-instruct', name: 'Llama 4 Scout 🦙', strengths: ['Meta', 'Exploration'], cost: 0 },\n { id: 'groq.openai/gpt-oss-120b', name: 'GPT-OSS 120B', strengths: ['OpenAI OSS', 'Très puissant'], cost: 0 },\n { id: 'groq.openai/gpt-oss-20b', name: 'GPT-OSS 20B', strengths: ['OpenAI OSS', 'Rapide'], cost: 0 },\n { id: 'groq.allam-2-7b', name: 'Allam 2 7B 🇸🇦', strengths: ['Arabe natif', 'SDAIA'], cost: 0 }\n ]\n },\n openrouter: {\n name: 'OpenRouter 🌐',\n models: [\n { id: 'openrouter.google/gemini-2.0-flash-exp:free', name: 'Gemini 2.0 Flash (Free)', strengths: ['Gratuit', 'Google', 'Rapide'], cost: 0 },\n { id: 'openrouter.meta-llama/llama-3.2-3b-instruct:free', name: 'Llama 3.2 3B (Free)', strengths: ['Gratuit', 'Meta', 'Compact'], cost: 0 },\n { id: 'openrouter.mistralai/mistral-7b-instruct:free', name: 'Mistral 7B (Free)', strengths: ['Gratuit', 'FR natif'], cost: 0 },\n { id: 'openrouter.deepseek/deepseek-chat', name: 'DeepSeek Chat 🇨🇳', strengths: ['Économique', 'Code', 'Raisonnement'], cost: 2 },\n { id: 'openrouter.deepseek/deepseek-r1', name: 'DeepSeek R1 🧠', strengths: ['Raisonnement avancé', 'Math'], cost: 5 },\n { id: 'openrouter.qwen/qwen-2.5-72b-instruct', name: 'Qwen 2.5 72B', strengths: ['Alibaba', 'Puissant'], cost: 10 },\n { id: 'openrouter.anthropic/claude-3.5-sonnet', name: 'Claude 3.5 Sonnet', strengths: ['Anthropic', 'Écriture'], cost: 15 },\n { id: 'openrouter.openai/gpt-4o-mini', name: 'GPT-4o Mini', strengths: ['OpenAI', 'Rapide'], cost: 5 },\n { id: 'openrouter.openai/gpt-4o', name: 'GPT-4o', strengths: ['OpenAI', 'Meilleur'], cost: 25 }\n ]\n },\n ollama: {\n name: 'Ollama 🏠 (lent)',\n models: [\n { id: 'ollama.llama3.2:3b', name: 'Llama 3.2 3B', strengths: ['Local', 'Privé', 'Rapide'], cost: 0 },\n { id: 'ollama.mistral:7b', name: 'Mistral 7B', strengths: ['Local', 'FR natif', 'Équilibré'], cost: 0 },\n { id: 'ollama.qwen2.5:3b', name: 'Qwen 2.5 3B', strengths: ['Local', 'Multilingue', 'Compact'], cost: 0 }\n ]\n }\n };\n\n // NOTE: ⚡ = Groq ultra-rapide, 🌐 = OpenRouter (gratuit/payant), 🏠 = Local (lent sur VPS)\n\n const PRESETS = {\n openrouter: {\n name: 'OpenRouter Free',\n experts: [\n { provider: 'openrouter', model: 'openrouter.google/gemini-2.0-flash-exp:free' },\n { provider: 'openrouter', model: 'openrouter.meta-llama/llama-3.2-3b-instruct:free' },\n { provider: 'openrouter', model: 'openrouter.mistralai/mistral-7b-instruct:free' }\n ],\n chairman: { provider: 'openrouter', model: 'openrouter.google/gemini-2.0-flash-exp:free' }\n },\n fast: {\n name: 'Groq Ultra Rapide',\n experts: [\n { provider: 'groq', model: 'groq.llama-3.3-70b-versatile' },\n { provider: 'groq', model: 'groq.qwen/qwen3-32b' },\n { provider: 'groq', model: 'groq.moonshotai/kimi-k2-instruct' }\n ],\n chairman: { provider: 'groq', model: 'groq.llama-3.3-70b-versatile' }\n },\n diversified: {\n name: 'Diversifié',\n experts: [\n { provider: 'groq', model: 'groq.llama-3.3-70b-versatile' },\n { provider: 'groq', model: 'groq.moonshotai/kimi-k2-instruct' },\n { provider: 'deepseek', model: 'deepseek.deepseek-chat' }\n ],\n chairman: { provider: 'deepseek', model: 'deepseek.deepseek-reasoner' }\n },\n deepseek: {\n name: 'DeepSeek Power 🇨🇳',\n experts: [\n { provider: 'deepseek', model: 'deepseek.deepseek-chat' },\n { provider: 'deepseek', model: 'deepseek.deepseek-reasoner' },\n { provider: 'groq', model: 'groq.llama-3.3-70b-versatile' }\n ],\n chairman: { provider: 'deepseek', model: 'deepseek.deepseek-reasoner' }\n },\n llama4: {\n name: 'Llama 4 🦙',\n experts: [\n { provider: 'groq', model: 'groq.meta-llama/llama-4-maverick-17b-128e-instruct' },\n { provider: 'groq', model: 'groq.meta-llama/llama-4-scout-17b-16e-instruct' },\n { provider: 'groq', model: 'groq.llama-3.3-70b-versatile' }\n ],\n chairman: { provider: 'groq', model: 'groq.meta-llama/llama-4-maverick-17b-128e-instruct' }\n },\n hybrid: {\n name: 'Hybride',\n experts: [\n { provider: 'groq', model: 'groq.llama-3.3-70b-versatile' },\n { provider: 'openrouter', model: 'openrouter.google/gemini-2.0-flash-exp:free' },\n { provider: 'groq', model: 'groq.qwen/qwen3-32b' }\n ],\n chairman: { provider: 'groq', model: 'groq.moonshotai/kimi-k2-instruct' }\n },\n local: {\n name: '100% Local',\n experts: [\n { provider: 'ollama', model: 'ollama.llama3.2:3b' },\n { provider: 'ollama', model: 'ollama.mistral:7b' },\n { provider: 'ollama', model: 'ollama.qwen2.5:3b' }\n ],\n chairman: { provider: 'ollama', model: 'ollama.mistral:7b' }\n }\n };\n\n let currentOpinions = {};\n\n // ============================================\n // Initialization\n // ============================================\n document.addEventListener('DOMContentLoaded', () => {\n // Load theme\n const savedTheme = localStorage.getItem('council-theme') || 'dark';\n document.documentElement.setAttribute('data-theme', savedTheme);\n updateThemeIcon(savedTheme);\n\n // Initialize all expert selects\n [1, 2, 3, 'Chairman'].forEach(num => {\n onProviderChange(num);\n });\n\n updateEstimate();\n });\n\n // ============================================\n // Theme Toggle\n // ============================================\n function toggleTheme() {\n const html = document.documentElement;\n const currentTheme = html.getAttribute('data-theme');\n const newTheme = currentTheme === 'dark' ? 'light' : 'dark';\n html.setAttribute('data-theme', newTheme);\n localStorage.setItem('council-theme', newTheme);\n updateThemeIcon(newTheme);\n }\n\n function updateThemeIcon(theme) {\n const icon = document.getElementById('theme-icon');\n icon.className = theme === 'dark' ? 'fas fa-moon' : 'fas fa-sun';\n }\n\n // ============================================\n // Provider/Model Management\n // ============================================\n function onProviderChange(expertNum) {\n const providerSelect = document.getElementById(`provider${expertNum}`);\n const modelSelect = document.getElementById(`model${expertNum}`);\n const strengthsBox = document.getElementById(`strengths${expertNum}`);\n \n const provider = providerSelect.value;\n const models = PROVIDERS[provider].models;\n\n // Populate models\n modelSelect.innerHTML = models.map(m => \n `<option value=\"${m.id}\">${m.name}</option>`\n ).join('');\n\n // Update strengths\n updateStrengths(expertNum);\n \n // Add change listener to model\n modelSelect.onchange = () => updateStrengths(expertNum);\n\n updateEstimate();\n }\n\n function updateStrengths(expertNum) {\n const providerSelect = document.getElementById(`provider${expertNum}`);\n const modelSelect = document.getElementById(`model${expertNum}`);\n const strengthsBox = document.getElementById(`strengths${expertNum}`);\n\n const provider = providerSelect.value;\n const modelId = modelSelect.value;\n const model = PROVIDERS[provider].models.find(m => m.id === modelId);\n\n if (model) {\n strengthsBox.textContent = `💪 ${model.strengths.join(', ')}`;\n }\n\n updateEstimate();\n }\n\n // ============================================\n // Presets\n // ============================================\n function applyPreset(presetName) {\n const preset = PRESETS[presetName];\n \n // Update presets UI\n document.querySelectorAll('.preset-card').forEach(card => {\n card.classList.remove('active');\n });\n event.currentTarget.classList.add('active');\n\n // Apply expert configs\n preset.experts.forEach((exp, idx) => {\n const num = idx + 1;\n document.getElementById(`provider${num}`).value = exp.provider;\n onProviderChange(num);\n document.getElementById(`model${num}`).value = exp.model;\n updateStrengths(num);\n });\n\n // Apply chairman config\n document.getElementById('providerChairman').value = preset.chairman.provider;\n onProviderChange('Chairman');\n document.getElementById('modelChairman').value = preset.chairman.model;\n updateStrengths('Chairman');\n\n updateEstimate();\n }\n\n // ============================================\n // Estimation\n // ============================================\n function updateEstimate() {\n let totalCost = 0;\n let fastestTime = true;\n\n [1, 2, 3, 'Chairman'].forEach(num => {\n const provider = document.getElementById(`provider${num}`).value;\n const modelId = document.getElementById(`model${num}`).value;\n const model = PROVIDERS[provider]?.models.find(m => m.id === modelId);\n \n if (model) {\n totalCost += model.cost;\n if (provider !== 'groq') fastestTime = false;\n }\n });\n\n const enableReview = document.getElementById('enableReview').checked;\n if (enableReview) {\n totalCost *= 1.5;\n }\n\n // Update UI\n document.getElementById('costEstimate').textContent = totalCost === 0 ? 'Gratuit!' : `~${Math.round(totalCost)} DZD`;\n document.getElementById('timeEstimate').textContent = fastestTime ? '~5s' : (enableReview ? '~30s' : '~15s');\n \n // Show warning for expensive configs\n document.getElementById('warningBox').style.display = totalCost > 100 ? 'block' : 'none';\n }\n\n // ============================================\n // Execute Council\n // ============================================\n async function executeCouncil() {\n const query = document.getElementById('queryInput').value.trim();\n if (!query) {\n showError('Veuillez entrer une question');\n return;\n }\n\n // Get configuration\n const experts = [1, 2, 3].map(num => ({\n provider: document.getElementById(`provider${num}`).value,\n model: document.getElementById(`model${num}`).value\n }));\n \n const chairman = {\n provider: document.getElementById('providerChairman').value,\n model: document.getElementById('modelChairman').value\n };\n\n const enableReview = document.getElementById('enableReview').checked;\n\n // Show loading\n document.getElementById('loadingBox').style.display = 'block';\n document.getElementById('resultsSection').style.display = 'none';\n document.getElementById('submitBtn').disabled = true;\n\n const startTime = Date.now();\n\n try {\n // Call each expert in parallel\n document.getElementById('loadingStatus').textContent = 'Les experts analysent votre question...';\n \n const expertPromises = experts.map((exp, idx) => \n callLLM(exp.model, query, `Expert ${idx + 1}`)\n );\n\n const expertResponses = await Promise.all(expertPromises);\n\n // Store opinions\n currentOpinions = {};\n expertResponses.forEach((response, idx) => {\n currentOpinions[`expert${idx + 1}`] = response;\n });\n\n // Chairman synthesis\n document.getElementById('loadingStatus').textContent = 'Le Chairman synthétise les réponses...';\n \n const synthesisPrompt = `Tu es le Chairman d'un conseil d'experts IA. \nVoici la question posée: \"${query}\"\n\nVoici les opinions des experts:\n\nExpert 1 (${getModelName(experts[0].model)}):\n${expertResponses[0]}\n\nExpert 2 (${getModelName(experts[1].model)}):\n${expertResponses[1]}\n\nExpert 3 (${getModelName(experts[2].model)}):\n${expertResponses[2]}\n\nEn tant que Chairman, fais une synthèse complète qui:\n1. Identifie les points de consensus\n2. Note les perspectives uniques de chaque expert\n3. Propose une réponse finale équilibrée et complète\n\nRéponds en français de manière structurée.`;\n\n const finalResponse = await callLLM(chairman.model, synthesisPrompt, 'Chairman');\n\n // Calculate stats\n const execTime = ((Date.now() - startTime) / 1000).toFixed(1);\n\n // Display results\n displayResults({\n finalResponse,\n chairman,\n experts,\n execTime,\n opinions: currentOpinions\n });\n\n } catch (error) {\n showError(`Erreur: ${error.message}`);\n } finally {\n document.getElementById('loadingBox').style.display = 'none';\n document.getElementById('submitBtn').disabled = false;\n }\n }\n\n async function callLLM(model, prompt, role) {\n try {\n // Check if it's an Ollama model\n if (model.startsWith('ollama.')) {\n return await callOllama(model, prompt, role);\n }\n\n // Check if it's an OpenRouter model\n if (model.startsWith('openrouter.')) {\n return await callOpenRouter(model, prompt, role);\n }\n\n // Check if it's a Groq model - call directly\n if (model.startsWith('groq.')) {\n return await callGroq(model, prompt, role);\n }\n\n // Check if it's a DeepSeek model - call directly\n if (model.startsWith('deepseek.')) {\n return await callDeepSeek(model, prompt, role);\n }\n\n // Standard API call for other providers (fallback)\n const response = await fetch('/api/llm/chat', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n model: model,\n messages: [{ role: 'user', content: prompt }],\n temperature: 0.7,\n max_tokens: 2000\n })\n });\n\n const data = await response.json();\n \n if (!response.ok || !data.success) {\n let errorMsg = 'Erreur inconnue';\n if (typeof data.detail === 'string') {\n errorMsg = data.detail;\n } else if (typeof data.error === 'string') {\n errorMsg = data.error;\n } else if (data.detail && typeof data.detail === 'object') {\n errorMsg = JSON.stringify(data.detail);\n } else if (data.error && typeof data.error === 'object') {\n errorMsg = JSON.stringify(data.error);\n }\n throw new Error(`${role}: ${errorMsg}`);\n }\n\n return data.answer || data.response || 'Réponse vide';\n } catch (fetchError) {\n if (fetchError.message.includes(role)) {\n throw fetchError;\n }\n throw new Error(`${role}: ${fetchError.message}`);\n }\n }\n\n async function callOllama(model, prompt, role) {\n // Extract actual model name (e.g., \"ollama.mistral:7b\" -> \"mistral:7b\")\n const ollamaModel = model.replace('ollama.', '');\n const systemPrompt = 'You MUST respond in the same language as the user input. Match the language exactly.';\n \n try {\n const response = await fetch('/ollama/api/generate', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n model: ollamaModel,\n prompt: prompt,\n system: systemPrompt,\n stream: false\n })\n });\n\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}`);\n }\n\n const data = await response.json();\n return data.response || 'Réponse vide';\n } catch (error) {\n throw new Error(`${role} (Ollama): ${error.message}`);\n }\n }\n\n async function callOpenRouter(model, prompt, role) {\n // Extract actual model name (e.g., \"openrouter.deepseek/deepseek-chat\" -> \"deepseek/deepseek-chat\")\n const openrouterModel = model.replace('openrouter.', '');\n \n try {\n const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${OPENROUTER_API_KEY}`,\n 'HTTP-Referer': window.location.href,\n 'X-Title': 'IAFactory Council'\n },\n body: JSON.stringify({\n model: openrouterModel,\n messages: [\n { role: 'system', content: 'You MUST respond in the same language as the user input. Match the language exactly.' },\n { role: 'user', content: prompt }\n ],\n max_tokens: 2000,\n temperature: 0.7\n })\n });\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n const errorMsg = errorData.error?.message || `HTTP ${response.status}`;\n throw new Error(errorMsg);\n }\n\n const data = await response.json();\n return data.choices?.[0]?.message?.content || 'Réponse vide';\n } catch (error) {\n throw new Error(`${role} (OpenRouter): ${error.message}`);\n }\n }\n\n async function callGroq(model, prompt, role) {\n // Extract actual model name (e.g., \"groq.llama-3.3-70b-versatile\" -> \"llama-3.3-70b-versatile\")\n const groqModel = model.replace('groq.', '');\n \n try {\n const response = await fetch('https://api.groq.com/openai/v1/chat/completions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${GROQ_API_KEY}`\n },\n body: JSON.stringify({\n model: groqModel,\n messages: [\n { role: 'system', content: 'You MUST respond in the same language as the user input. Match the language exactly.' },\n { role: 'user', content: prompt }\n ],\n max_tokens: 2000,\n temperature: 0.7\n })\n });\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n const errorMsg = errorData.error?.message || `HTTP ${response.status}`;\n throw new Error(errorMsg);\n }\n\n const data = await response.json();\n return data.choices?.[0]?.message?.content || 'Réponse vide';\n } catch (error) {\n throw new Error(`${role} (Groq): ${error.message}`);\n }\n }\n\n async function callDeepSeek(model, prompt, role) {\n // Extract actual model name (e.g., \"deepseek.deepseek-chat\" -> \"deepseek-chat\")\n const deepseekModel = model.replace('deepseek.', '');\n \n try {\n const response = await fetch('https://api.deepseek.com/v1/chat/completions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${DEEPSEEK_API_KEY}`\n },\n body: JSON.stringify({\n model: deepseekModel,\n messages: [\n { role: 'system', content: 'You MUST respond in the same language as the user input. If the user writes in French, respond in French. If in English, respond in English. Never respond in Chinese unless the user writes in Chinese.' },\n { role: 'user', content: prompt }\n ],\n max_tokens: 2000,\n temperature: 0.7\n })\n });\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n const errorMsg = errorData.error?.message || `HTTP ${response.status}`;\n throw new Error(errorMsg);\n }\n\n const data = await response.json();\n return data.choices?.[0]?.message?.content || 'Réponse vide';\n } catch (error) {\n throw new Error(`${role} (DeepSeek): ${error.message}`);\n }\n }\n\n function getModelName(modelId) {\n for (const provider of Object.values(PROVIDERS)) {\n const model = provider.models.find(m => m.id === modelId);\n if (model) return model.name;\n }\n return modelId;\n }\n\n // ============================================\n // Display Results\n // ============================================\n function displayResults(data) {\n document.getElementById('resultsSection').style.display = 'block';\n document.getElementById('finalResponse').textContent = data.finalResponse;\n document.getElementById('chairmanName').textContent = `(${getModelName(data.chairman.model)})`;\n document.getElementById('execTime').innerHTML = `<i class=\"fas fa-clock\"></i> ${data.execTime}s`;\n document.getElementById('actualCost').innerHTML = `<i class=\"fas fa-coins\"></i> Estimé`;\n document.getElementById('expertsCount').innerHTML = `<i class=\"fas fa-users\"></i> 3 experts + Chairman`;\n\n // Build opinion tabs\n const tabsHTML = data.experts.map((exp, idx) => `\n <button class=\"opinion-tab ${idx === 0 ? 'active' : ''}\" \n onclick=\"showOpinion('expert${idx + 1}', this)\">\n Expert ${idx + 1} (${getModelName(exp.model)})\n </button>\n `).join('');\n\n document.getElementById('opinionTabs').innerHTML = tabsHTML;\n showOpinion('expert1', document.querySelector('.opinion-tab'));\n\n // Scroll to results\n document.getElementById('resultsSection').scrollIntoView({ behavior: 'smooth' });\n }\n\n function showOpinion(expertKey, tabElement) {\n // Update tabs\n document.querySelectorAll('.opinion-tab').forEach(tab => tab.classList.remove('active'));\n tabElement.classList.add('active');\n\n // Show content\n document.getElementById('opinionContent').textContent = currentOpinions[expertKey] || 'Opinion non disponible';\n }\n\n // ============================================\n // Error Handling\n // ============================================\n function showError(message) {\n const toast = document.getElementById('errorToast');\n document.getElementById('errorMessage').textContent = message;\n toast.classList.add('visible');\n\n setTimeout(() => {\n toast.classList.remove('visible');\n }, 5000);\n }\n\n // Listen for review checkbox\n document.getElementById('enableReview').addEventListener('change', updateEstimate);\n </script>\n</body>\n</html>\n"}}, "apps/_archived/council/index.html": {"7d6e298b582d583185cd24b914c17d9daeaa9491": {"t": 1785747147.5006812, "n": 87686, "h": "f4c571a3f32cc4a8", "c": "<!DOCTYPE html>\n<html lang=\"fr\" data-theme=\"dark\" data-iaf-auto-init>\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Build Your Council - IA Factory Algeria</title>\n <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n <link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap\" rel=\"stylesheet\">\n <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css\">\n <style>\n :root {\n /* Dark Mode (default) - Aligné avec Landing Page */\n --bg-primary: #020617;\n --bg-secondary: #020617;\n --bg-card: #020617;\n --bg-card-hover: #0a0f1a;\n --border-color: rgba(255, 255, 255, 0.12);\n --text-primary: #f8fafc;\n --text-secondary: rgba(248, 250, 252, 0.75);\n --text-muted: rgba(248, 250, 252, 0.5);\n --accent-primary: #00a651;\n --accent-secondary: #00c95d;\n --accent-gradient: linear-gradient(135deg, #00a651, #008c45);\n --success: #10b981;\n --success-bg: rgba(16, 185, 129, 0.1);\n --warning: #f59e0b;\n --warning-bg: rgba(245, 158, 11, 0.1);\n --error: #ef4444;\n --input-bg: #0f172a;\n --input-border: rgba(255, 255, 255, 0.15);\n --input-text: #f8fafc;\n --input-placeholder: rgba(248, 250, 252, 0.5);\n }\n\n [data-theme=\"light\"] {\n --bg-primary: #f7f5f0;\n --bg-secondary: #f7f5f0;\n --bg-card: #f7f5f0;\n --bg-card-hover: #efe9df;\n --border-color: rgba(0, 0, 0, 0.08);\n --text-primary: #0f172a;\n --text-secondary: rgba(15, 23, 42, 0.7);\n --text-muted: rgba(15, 23, 42, 0.5);\n --accent-primary: #00a651;\n --accent-secondary: #008c45;\n --accent-gradient: linear-gradient(135deg, #00a651, #008c45);\n --success-bg: rgba(16, 185, 129, 0.1);\n --warning-bg: rgba(245, 158, 11, 0.1);\n --input-bg: #ffffff;\n --input-border: rgba(0, 0, 0, 0.12);\n --input-text: #0f172a;\n --input-placeholder: rgba(15, 23, 42, 0.5);\n }\n\n * {\n font-family: 'Inter', sans-serif;\n margin: 0;\n padding: 0;\n box-sizing: border-box;\n }\n\n body {\n background: var(--bg-primary);\n color: var(--text-primary);\n min-height: 100vh;\n transition: background 0.3s ease, color 0.3s ease;\n }\n\n /* Header */\n .main-header {\n background: var(--bg-secondary);\n border-bottom: 1px solid var(--border-color);\n padding: 1rem 2rem;\n display: flex;\n justify-content: space-between;\n align-items: center;\n position: sticky;\n top: 0;\n z-index: 100;\n }\n\n .logo {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n }\n\n .logo-icon {\n width: 40px;\n height: 40px;\n background: var(--accent-gradient);\n border-radius: 10px;\n display: flex;\n align-items: center;\n justify-content: center;\n color: white;\n font-size: 1.25rem;\n }\n\n .logo h1 {\n font-size: 1.25rem;\n font-weight: 700;\n background: var(--accent-gradient);\n -webkit-background-clip: text;\n -webkit-text-fill-color: transparent;\n }\n\n .header-actions {\n display: flex;\n align-items: center;\n gap: 1rem;\n }\n\n .theme-toggle {\n background: var(--bg-card);\n border: 1px solid var(--border-color);\n color: var(--text-primary);\n padding: 0.5rem 0.75rem;\n border-radius: 8px;\n cursor: pointer;\n font-size: 1rem;\n transition: all 0.2s;\n }\n\n .theme-toggle:hover {\n background: var(--bg-card-hover);\n border-color: var(--accent-primary);\n }\n\n .home-btn {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n padding: 0.5rem 1rem;\n background: var(--accent-gradient);\n color: white;\n border: none;\n border-radius: 8px;\n font-size: 0.875rem;\n font-weight: 500;\n cursor: pointer;\n text-decoration: none;\n transition: all 0.2s;\n }\n\n .home-btn:hover {\n transform: translateY(-1px);\n box-shadow: 0 4px 12px rgba(124, 58, 237, 0.3);\n }\n\n /* Main Container */\n .container {\n max-width: 1400px;\n margin: 0 auto;\n padding: 2rem;\n }\n\n /* Page Header */\n .page-header {\n text-align: center;\n margin-bottom: 2rem;\n }\n\n .page-header h1 {\n font-size: 2.5rem;\n font-weight: 800;\n margin-bottom: 0.5rem;\n background: var(--accent-gradient);\n -webkit-background-clip: text;\n -webkit-text-fill-color: transparent;\n }\n\n .page-header p {\n font-size: 1.1rem;\n color: var(--text-secondary);\n }\n\n /* Presets */\n .presets-grid {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));\n gap: 1rem;\n margin: 1.5rem 0;\n }\n\n .preset-card {\n padding: 1.25rem;\n background: var(--bg-card);\n border: 2px solid var(--border-color);\n border-radius: 12px;\n cursor: pointer;\n transition: all 0.2s;\n text-align: center;\n }\n\n .preset-card:hover {\n transform: translateY(-2px);\n border-color: var(--accent-primary);\n box-shadow: 0 4px 12px rgba(0, 166, 81, 0.25);\n }\n\n .preset-card.active {\n border-color: var(--accent-primary);\n background: rgba(0, 166, 81, 0.1);\n }\n\n .preset-card.recommended {\n border: 2px solid var(--accent-primary);\n }\n\n .preset-icon {\n font-size: 2rem;\n margin-bottom: 0.5rem;\n }\n\n .preset-name {\n font-weight: 600;\n margin-bottom: 0.25rem;\n color: var(--text-primary);\n }\n\n .preset-desc {\n font-size: 0.75rem;\n color: var(--text-secondary);\n }\n\n /* Config Sections */\n .config-section {\n background: var(--bg-card);\n border: 1px solid var(--border-color);\n border-radius: 16px;\n padding: 1.5rem;\n margin-bottom: 1.5rem;\n }\n\n .section-title {\n font-size: 1.125rem;\n font-weight: 700;\n margin-bottom: 1.5rem;\n display: flex;\n align-items: center;\n gap: 0.5rem;\n color: var(--text-primary);\n }\n\n /* Experts Grid */\n .experts-grid {\n display: grid;\n grid-template-columns: repeat(4, 1fr);\n gap: 1.5rem;\n }\n\n @media (max-width: 1200px) {\n .experts-grid {\n grid-template-columns: repeat(2, 1fr);\n }\n }\n\n @media (max-width: 600px) {\n .experts-grid {\n grid-template-columns: 1fr;\n }\n }\n\n .expert-card {\n background: var(--bg-secondary);\n border: 2px solid var(--border-color);\n border-radius: 12px;\n padding: 1.25rem;\n transition: all 0.2s;\n }\n\n .expert-card:hover {\n border-color: var(--accent-secondary);\n }\n\n .expert-card.expert-1 { border-top: 3px solid var(--accent-primary); }\n .expert-card.expert-2 { border-top: 3px solid var(--accent-primary); }\n .expert-card.expert-3 { border-top: 3px solid var(--accent-primary); }\n .expert-card.chairman { border-top: 3px solid var(--accent-secondary); }\n\n .expert-label {\n font-weight: 700;\n margin-bottom: 1rem;\n font-size: 0.95rem;\n display: flex;\n align-items: center;\n gap: 0.5rem;\n }\n\n .expert-1 .expert-label { color: var(--accent-primary); }\n .expert-2 .expert-label { color: var(--accent-primary); }\n .expert-3 .expert-label { color: var(--accent-primary); }\n .chairman .expert-label { color: var(--accent-secondary); }\n\n .form-group {\n margin-bottom: 1rem;\n }\n\n .form-group:last-child {\n margin-bottom: 0;\n }\n\n .form-group label {\n display: block;\n font-size: 0.75rem;\n font-weight: 600;\n color: var(--text-secondary);\n margin-bottom: 0.4rem;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n }\n\n .form-group select {\n width: 100%;\n padding: 0.75rem 1rem;\n background: var(--bg-card);\n border: 1px solid var(--border-color);\n border-radius: 8px;\n font-size: 0.875rem;\n color: var(--text-primary);\n cursor: pointer;\n transition: all 0.2s;\n appearance: none;\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%2394a3b8'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E\");\n background-repeat: no-repeat;\n background-position: right 0.75rem center;\n background-size: 1rem;\n padding-right: 2.5rem;\n }\n\n .form-group select:focus {\n outline: none;\n border-color: var(--accent-primary);\n box-shadow: 0 0 0 3px rgba(124, 58, 237, 0.15);\n }\n\n .form-group select option {\n background: var(--bg-card);\n color: var(--text-primary);\n padding: 0.5rem;\n }\n\n .strengths-box {\n font-size: 0.8rem;\n color: var(--text-secondary);\n padding: 0.5rem 0.75rem;\n background: rgba(0, 166, 81, 0.1);\n border-radius: 6px;\n margin-top: 0.75rem;\n }\n\n /* Estimate Box */\n .estimate-box {\n background: var(--accent-gradient);\n color: white;\n padding: 1.5rem;\n border-radius: 12px;\n margin-bottom: 1.5rem;\n }\n\n .estimate-title {\n font-size: 1.1rem;\n font-weight: 700;\n margin-bottom: 1rem;\n }\n\n .estimate-grid {\n display: grid;\n grid-template-columns: repeat(3, 1fr);\n gap: 1rem;\n }\n\n @media (max-width: 600px) {\n .estimate-grid {\n grid-template-columns: 1fr;\n }\n }\n\n .estimate-item {\n background: rgba(255,255,255,0.15);\n padding: 1rem;\n border-radius: 8px;\n text-align: center;\n }\n\n .estimate-label {\n font-size: 0.85rem;\n opacity: 0.9;\n }\n\n .estimate-value {\n font-size: 1.5rem;\n font-weight: 700;\n margin-top: 0.25rem;\n }\n\n .warning-box {\n display: none;\n margin-top: 1rem;\n padding: 0.75rem;\n background: rgba(245, 158, 11, 0.2);\n border-radius: 8px;\n font-size: 0.875rem;\n }\n\n /* Query Section */\n .query-section textarea {\n width: 100%;\n min-height: 150px;\n padding: 1rem;\n background: var(--input-bg);\n border: 2px solid var(--input-border);\n border-radius: 12px;\n font-size: 0.95rem;\n color: var(--input-text);\n resize: vertical;\n transition: all 0.2s;\n line-height: 1.6;\n }\n\n .query-section textarea::placeholder {\n color: var(--input-placeholder);\n }\n\n .query-section textarea:focus {\n outline: none;\n border-color: var(--accent-primary);\n box-shadow: 0 0 0 3px rgba(124, 58, 237, 0.15);\n }\n\n /* Options Row */\n .options-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n margin-top: 1rem;\n flex-wrap: wrap;\n gap: 1rem;\n }\n\n .checkbox-label {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n cursor: pointer;\n font-size: 0.9rem;\n color: var(--text-secondary);\n }\n\n .checkbox-label input[type=\"checkbox\"] {\n width: 1.25rem;\n height: 1.25rem;\n cursor: pointer;\n accent-color: var(--accent-primary);\n }\n\n /* Submit Button */\n .btn-submit {\n padding: 1rem 2rem;\n background: var(--accent-gradient);\n color: white;\n border: none;\n border-radius: 10px;\n font-size: 1rem;\n font-weight: 600;\n cursor: pointer;\n transition: all 0.2s;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 0.5rem;\n }\n\n .btn-submit:hover:not(:disabled) {\n transform: translateY(-1px);\n box-shadow: 0 6px 20px rgba(124, 58, 237, 0.35);\n }\n\n .btn-submit:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n }\n\n /* Loading */\n .loading-box {\n display: none;\n padding: 1.5rem;\n background: var(--warning-bg);\n border-left: 4px solid var(--warning);\n border-radius: 8px;\n margin-bottom: 1.5rem;\n }\n\n .loading-content {\n display: flex;\n align-items: center;\n gap: 1rem;\n }\n\n .spinner {\n border: 3px solid rgba(245, 158, 11, 0.3);\n border-top-color: var(--warning);\n border-radius: 50%;\n width: 24px;\n height: 24px;\n animation: spin 0.8s linear infinite;\n }\n\n @keyframes spin {\n to { transform: rotate(360deg); }\n }\n\n /* Results Section */\n .results-section {\n display: none;\n }\n\n .final-response-box {\n background: linear-gradient(135deg, rgba(16, 185, 129, 0.15), rgba(5, 150, 105, 0.1));\n border: 1px solid rgba(16, 185, 129, 0.3);\n border-left: 4px solid var(--success);\n border-radius: 16px;\n padding: 2rem;\n margin-bottom: 1.5rem;\n }\n\n .result-header {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n margin-bottom: 1rem;\n font-size: 1.25rem;\n font-weight: 700;\n color: var(--success);\n }\n\n .result-text {\n font-size: 1rem;\n line-height: 1.8;\n color: var(--text-primary);\n white-space: pre-wrap;\n }\n\n .result-meta {\n margin-top: 1.5rem;\n padding-top: 1rem;\n border-top: 1px solid rgba(16, 185, 129, 0.2);\n display: flex;\n gap: 2rem;\n font-size: 0.875rem;\n color: var(--text-secondary);\n }\n\n /* Opinions Section */\n .opinions-box {\n background: var(--bg-card);\n border: 1px solid var(--border-color);\n border-radius: 16px;\n padding: 1.5rem;\n }\n\n .opinion-tabs {\n display: flex;\n gap: 0.5rem;\n margin-bottom: 1.5rem;\n border-bottom: 2px solid var(--border-color);\n padding-bottom: 0.5rem;\n overflow-x: auto;\n }\n\n .opinion-tab {\n padding: 0.75rem 1.25rem;\n background: transparent;\n border: none;\n border-bottom: 3px solid transparent;\n cursor: pointer;\n font-weight: 500;\n color: var(--text-secondary);\n transition: all 0.2s;\n white-space: nowrap;\n }\n\n .opinion-tab:hover {\n color: var(--text-primary);\n }\n\n .opinion-tab.active {\n color: var(--accent-primary);\n border-bottom-color: var(--accent-primary);\n }\n\n .opinion-content {\n padding: 1.5rem;\n background: var(--bg-secondary);\n border-radius: 12px;\n line-height: 1.8;\n white-space: pre-wrap;\n color: var(--text-primary);\n max-height: 400px;\n overflow-y: auto;\n }\n\n /* Error Toast */\n .error-toast {\n position: fixed;\n bottom: 2rem;\n right: 2rem;\n padding: 1rem 1.5rem;\n background: var(--error);\n color: white;\n border-radius: 10px;\n font-size: 0.875rem;\n display: none;\n z-index: 1000;\n animation: slideIn 0.3s ease;\n max-width: 400px;\n }\n\n .error-toast.visible {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n }\n\n @keyframes slideIn {\n from {\n transform: translateY(20px);\n opacity: 0;\n }\n to {\n transform: translateY(0);\n opacity: 1;\n }\n }\n\n /* Scrollbar */\n ::-webkit-scrollbar {\n width: 8px;\n height: 8px;\n }\n\n ::-webkit-scrollbar-track {\n background: transparent;\n }\n\n ::-webkit-scrollbar-thumb {\n background: var(--border-color);\n border-radius: 4px;\n }\n\n ::-webkit-scrollbar-thumb:hover {\n background: var(--text-muted);\n }\n </style>\n <link rel=\"stylesheet\" href=\"/apps/shared/iafactory-unified.css\">\n <style>\n\n :root {\n --bg: #020617;\n --bg-alt: #020617;\n --card: #020617;\n --bg-card: #020617;\n --bg-secondary: #0f172a;\n --bg-dark: #020617;\n --border: rgba(255, 255, 255, 0.12);\n --border-primary: rgba(255, 255, 255, 0.12);\n --primary: #00a651;\n --primary-dark: #008c45;\n --accent: #00a651;\n --accent-primary: #00a651;\n --text: #f8fafc;\n --text-primary: #f8fafc;\n --text-secondary: rgba(248, 250, 252, 0.75);\n --muted: rgba(248, 250, 252, 0.75);\n --shadow: 0 20px 60px rgba(0, 0, 0, 0.55);\n }\n\n [data-theme=\"light\"] {\n --bg: #f7f5f0;\n --bg-alt: #f7f5f0;\n --card: #f7f5f0;\n --bg-card: #ffffff;\n --bg-secondary: #f7f5f0;\n --bg-dark: #f7f5f0;\n --border: rgba(0, 0, 0, 0.08);\n --border-primary: rgba(0, 0, 0, 0.08);\n --primary: #00a651;\n --primary-dark: #008c45;\n --accent: #00a651;\n --accent-primary: #00a651;\n --text: #0f172a;\n --text-primary: #0f172a;\n --text-secondary: rgba(15, 23, 42, 0.7);\n --muted: rgba(15, 23, 42, 0.7);\n --shadow: 0 20px 60px rgba(15, 23, 42, 0.25);\n }\n </style>\n</head>\n<body>\n <!-- Header -->\n <header class=\"main-header\">\n <div class=\"logo\">\n <div class=\"logo-icon\">\n <i class=\"fas fa-users\"></i>\n </div>\n <h1>Council IA</h1>\n </div>\n <div class=\"header-actions\">\n <button class=\"theme-toggle\" onclick=\"toggleTheme()\" title=\"Changer le thème\">\n <i class=\"fas fa-moon\" id=\"theme-icon\"></i>\n </button>\n <a href=\"/\" class=\"home-btn\">\n <i class=\"fas fa-home\"></i>\n Accueil\n </a>\n </div>\n \n <!-- IAF Unified Controls -->\n <div class=\"iaf-header-actions\" style=\"display:flex;align-items:center;gap:0.5rem;margin-left:auto;\">\n <div class=\"iaf-lang-dropdown\" style=\"position:relative;\">\n <button class=\"iaf-lang-btn\" onclick=\"IAFactory.toggleLangMenu()\" style=\"display:flex;align-items:center;gap:0.5rem;padding:0.5rem 0.75rem;border-radius:8px;border:1px solid rgba(255,255,255,0.1);background:transparent;color:inherit;cursor:pointer;font-size:0.85rem;\">\n 🌐 <span id=\"iaf-current-lang\">FR</span>\n </button>\n <div class=\"iaf-lang-menu\" id=\"iaf-lang-menu\" style=\"position:absolute;top:100%;right:0;margin-top:4px;background:var(--bg-card,#1a1a2e);border:1px solid rgba(255,255,255,0.1);border-radius:12px;box-shadow:0 15px 30px rgba(0,0,0,0.3);display:none;min-width:150px;z-index:1001;overflow:hidden;\">\n <button class=\"iaf-lang-option\" onclick=\"IAFactory.setLanguage('fr')\" style=\"display:flex;align-items:center;gap:0.5rem;width:100%;padding:0.75rem 1rem;border:none;background:transparent;color:inherit;cursor:pointer;text-align:left;\">🇫🇷 Français</button>\n <button class=\"iaf-lang-option\" onclick=\"IAFactory.setLanguage('en')\" style=\"display:flex;align-items:center;gap:0.5rem;width:100%;padding:0.75rem 1rem;border:none;background:transparent;color:inherit;cursor:pointer;text-align:left;\">🇬🇧 English</button>\n <button class=\"iaf-lang-option\" onclick=\"IAFactory.setLanguage('ar')\" style=\"display:flex;align-items:center;gap:0.5rem;width:100%;padding:0.75rem 1rem;border:none;background:transparent;color:inherit;cursor:pointer;text-align:left;\">🇩🇿 العربية</button>\n </div>\n </div>\n <button class=\"iaf-theme-btn\" onclick=\"IAFactory.toggleTheme()\" id=\"iaf-theme-btn\" style=\"width:40px;height:40px;border-radius:10px;border:1px solid rgba(255,255,255,0.1);background:transparent;color:inherit;cursor:pointer;font-size:1.1rem;\">☀️</button>\n </div>\n\n </header>\n\n <div class=\"container\">\n <!-- Page Header -->\n <div class=\"page-header\">\n <h1>🎯 Build Your Council</h1>\n <p>Composez votre équipe d'experts IA sur mesure avec une infinité de combinaisons</p>\n </div>\n\n <!-- Presets -->\n <div class=\"config-section\">\n <h2 class=\"section-title\">✨ Configurations recommandées (Groq ⚡ + OpenRouter 🌐)</h2>\n <div class=\"presets-grid\" id=\"presetsGrid\">\n <div class=\"preset-card recommended\" onclick=\"applyPreset('fast')\">\n <div class=\"preset-icon\">⚡</div>\n <div class=\"preset-name\">Groq Diversifié ⭐</div>\n <div class=\"preset-desc\">Llama 70B + Qwen + Kimi</div>\n </div>\n <div class=\"preset-card recommended\" onclick=\"applyPreset('deepseek')\">\n <div class=\"preset-icon\">🇨🇳</div>\n <div class=\"preset-name\">DeepSeek Power ⭐</div>\n <div class=\"preset-desc\">Chat + R1 Reasoner</div>\n </div>\n <div class=\"preset-card\" onclick=\"applyPreset('diversified')\">\n <div class=\"preset-icon\">🎯</div>\n <div class=\"preset-name\">Mix Groq+DeepSeek</div>\n <div class=\"preset-desc\">Llama + Kimi + DeepSeek</div>\n </div>\n <div class=\"preset-card\" onclick=\"applyPreset('llama4')\">\n <div class=\"preset-icon\">🦙</div>\n <div class=\"preset-name\">Llama 4 Meta</div>\n <div class=\"preset-desc\">Maverick + Scout + 70B</div>\n </div>\n <div class=\"preset-card\" onclick=\"applyPreset('openrouter')\">\n <div class=\"preset-icon\">🌐</div>\n <div class=\"preset-name\">OpenRouter FREE</div>\n <div class=\"preset-desc\">Gemini + Llama + Mistral</div>\n </div>\n <div class=\"preset-card\" onclick=\"applyPreset('hybrid')\">\n <div class=\"preset-icon\">🔀</div>\n <div class=\"preset-name\">Hybride</div>\n <div class=\"preset-desc\">Groq + Gemini + Qwen</div>\n </div>\n <div class=\"preset-card\" onclick=\"applyPreset('local')\">\n <div class=\"preset-icon\">🏠</div>\n <div class=\"preset-name\">100% Local</div>\n <div class=\"preset-desc\">Ollama (lent sur VPS)</div>\n </div>\n </div>\n </div>\n\n <!-- Configuration -->\n <div class=\"config-section\">\n <h2 class=\"section-title\">⚙️ Configuration du Council</h2>\n \n <div class=\"experts-grid\">\n <!-- Expert 1 -->\n <div class=\"expert-card expert-1\">\n <div class=\"expert-label\">\n <i class=\"fas fa-user-tie\"></i> Expert 1\n </div>\n <div class=\"form-group\">\n <label>Provider</label>\n <select id=\"provider1\" onchange=\"onProviderChange(1)\">\n <option value=\"deepseek\">🇨🇳 DeepSeek (Économique)</option>\n <option value=\"groq\" selected>⚡ Groq (Ultra-rapide)</option>\n <option value=\"openrouter\">🌐 OpenRouter</option>\n <option value=\"ollama\">🏠 Ollama (Local)</option>\n <option value=\"openai\">🤖 OpenAI (ChatGPT)</option>\n <option value=\"anthropic\">🎭 Anthropic (Claude)</option>\n <option value=\"perplexity\">🔍 Perplexity</option>\n <option value=\"grok\">🚀 Grok (xAI)</option>\n <option value=\"mistral\">🇫🇷 Mistral</option>\n </select>\n </div>\n <div class=\"form-group\">\n <label>Modèle</label>\n <select id=\"model1\"></select>\n </div>\n <div class=\"strengths-box\" id=\"strengths1\">💪 Sélectionnez un modèle</div>\n </div>\n\n <!-- Expert 2 -->\n <div class=\"expert-card expert-2\">\n <div class=\"expert-label\">\n <i class=\"fas fa-user-tie\"></i> Expert 2\n </div>\n <div class=\"form-group\">\n <label>Provider</label>\n <select id=\"provider2\" onchange=\"onProviderChange(2)\">\n <option value=\"deepseek\">🇨🇳 DeepSeek (Économique)</option>\n <option value=\"groq\" selected>⚡ Groq (Ultra-rapide)</option>\n <option value=\"openrouter\">🌐 OpenRouter</option>\n <option value=\"ollama\">🏠 Ollama (Local)</option>\n <option value=\"openai\">🤖 OpenAI (ChatGPT)</option>\n <option value=\"anthropic\">🎭 Anthropic (Claude)</option>\n <option value=\"perplexity\">🔍 Perplexity</option>\n <option value=\"grok\">🚀 Grok (xAI)</option>\n <option value=\"mistral\">🇫🇷 Mistral</option>\n </select>\n </div>\n <div class=\"form-group\">\n <label>Modèle</label>\n <select id=\"model2\"></select>\n </div>\n <div class=\"strengths-box\" id=\"strengths2\">💪 Sélectionnez un modèle</div>\n </div>\n\n <!-- Expert 3 -->\n <div class=\"expert-card expert-3\">\n <div class=\"expert-label\">\n <i class=\"fas fa-user-tie\"></i> Expert 3\n </div>\n <div class=\"form-group\">\n <label>Provider</label>\n <select id=\"provider3\" onchange=\"onProviderChange(3)\">\n <option value=\"deepseek\">🇨🇳 DeepSeek (Économique)</option>\n <option value=\"groq\" selected>⚡ Groq (Ultra-rapide)</option>\n <option value=\"openrouter\">🌐 OpenRouter</option>\n <option value=\"ollama\">🏠 Ollama (Local)</option>\n <option value=\"openai\">🤖 OpenAI (ChatGPT)</option>\n <option value=\"anthropic\">🎭 Anthropic (Claude)</option>\n <option value=\"perplexity\">🔍 Perplexity</option>\n <option value=\"grok\">🚀 Grok (xAI)</option>\n <option value=\"mistral\">🇫🇷 Mistral</option>\n </select>\n </div>\n <div class=\"form-group\">\n <label>Modèle</label>\n <select id=\"model3\"></select>\n </div>\n <div class=\"strengths-box\" id=\"strengths3\">💪 Sélectionnez un modèle</div>\n </div>\n\n <!-- Chairman -->\n <div class=\"expert-card chairman\">\n <div class=\"expert-label\">\n <i class=\"fas fa-crown\"></i> Chairman (Synthèse)\n </div>\n <div class=\"form-group\">\n <label>Provider</label>\n <select id=\"providerChairman\" onchange=\"onProviderChange('Chairman')\">\n <option value=\"deepseek\">🇨🇳 DeepSeek (Économique)</option>\n <option value=\"groq\" selected>⚡ Groq (Ultra-rapide)</option>\n <option value=\"openrouter\">🌐 OpenRouter</option>\n <option value=\"ollama\">🏠 Ollama (Local)</option>\n <option value=\"openai\">🤖 OpenAI (ChatGPT)</option>\n <option value=\"anthropic\">🎭 Anthropic (Claude)</option>\n <option value=\"perplexity\">🔍 Perplexity</option>\n <option value=\"grok\">🚀 Grok (xAI)</option>\n <option value=\"mistral\">🇫🇷 Mistral</option>\n </select>\n </div>\n <div class=\"form-group\">\n <label>Modèle</label>\n <select id=\"modelChairman\"></select>\n </div>\n <div class=\"strengths-box\" id=\"strengthsChairman\">💪 Sélectionnez un modèle</div>\n </div>\n </div>\n </div>\n\n <!-- Estimation -->\n <div class=\"estimate-box\">\n <div class=\"estimate-title\">📊 Estimation</div>\n <div class=\"estimate-grid\">\n <div class=\"estimate-item\">\n <div class=\"estimate-label\">💰 Coût estimé</div>\n <div class=\"estimate-value\" id=\"costEstimate\">~50 DZD</div>\n </div>\n <div class=\"estimate-item\">\n <div class=\"estimate-label\">⏱️ Temps estimé</div>\n <div class=\"estimate-value\" id=\"timeEstimate\">~15s</div>\n </div>\n <div class=\"estimate-item\">\n <div class=\"estimate-label\">🎯 Combinaisons</div>\n <div class=\"estimate-value\">∞</div>\n </div>\n </div>\n <div class=\"warning-box\" id=\"warningBox\">\n ⚠️ Configuration premium : coût élevé. Utilisez Groq pour réduire les coûts.\n </div>\n </div>\n\n <!-- Query Section -->\n <div class=\"config-section query-section\">\n <h2 class=\"section-title\">💬 Votre question</h2>\n <textarea \n id=\"queryInput\" \n placeholder=\"Posez votre question au Council personnalisé...\n\nExemples:\n• Quelles sont les meilleures pratiques pour sécuriser une API REST ?\n• Comment optimiser PostgreSQL pour des millions de lignes ?\n• Expliquez la différence entre authentification et autorisation\n• Compare les architectures microservices vs monolithique\"\n ></textarea>\n \n <div class=\"options-row\">\n <label class=\"checkbox-label\">\n <input type=\"checkbox\" id=\"enableReview\">\n <span>Activer la revue croisée (+50% temps, meilleure qualité)</span>\n </label>\n <button class=\"btn-submit\" id=\"submitBtn\" onclick=\"executeCouncil()\">\n <i class=\"fas fa-bolt\"></i>\n Lancer le Council\n </button>\n </div>\n </div>\n\n <!-- Loading -->\n <div class=\"loading-box\" id=\"loadingBox\">\n <div class=\"loading-content\">\n <div class=\"spinner\"></div>\n <div>\n <div style=\"font-weight: 600; margin-bottom: 0.25rem;\">Council en délibération...</div>\n <div style=\"font-size: 0.875rem; color: var(--text-secondary);\" id=\"loadingStatus\">Les experts analysent votre question</div>\n </div>\n </div>\n </div>\n\n <!-- Results -->\n <div class=\"results-section\" id=\"resultsSection\">\n <div class=\"final-response-box\">\n <div class=\"result-header\">\n <i class=\"fas fa-crown\"></i>\n <span>Réponse finale du Chairman</span>\n <span id=\"chairmanName\" style=\"font-size: 0.9rem; font-weight: normal; color: var(--text-secondary);\"></span>\n </div>\n <div class=\"result-text\" id=\"finalResponse\"></div>\n <div class=\"result-meta\">\n <span id=\"execTime\"><i class=\"fas fa-clock\"></i> -</span>\n <span id=\"actualCost\"><i class=\"fas fa-coins\"></i> -</span>\n <span id=\"expertsCount\"><i class=\"fas fa-users\"></i> -</span>\n </div>\n </div>\n\n <div class=\"opinions-box\">\n <h3 class=\"section-title\">🎯 Opinions individuelles des experts</h3>\n <div class=\"opinion-tabs\" id=\"opinionTabs\"></div>\n <div class=\"opinion-content\" id=\"opinionContent\">Sélectionnez un expert pour voir son opinion</div>\n </div>\n </div>\n </div>\n\n <!-- Error Toast -->\n <div class=\"error-toast\" id=\"errorToast\">\n <i class=\"fas fa-exclamation-circle\"></i>\n <span id=\"errorMessage\"></span>\n </div>\n\n <script>\n // ============================================\n // Provider and Model Configuration\n // ============================================\n // API Keys pour appels directs\n // ============================================\n const OPENROUTER_API_KEY = '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';\n const GROQ_API_KEY = 'YOUR_GROQ_API_KEY';\n const DEEPSEEK_API_KEY = 'sk-e2d7d214600946479856ffafbe1ce392';\n const OPENAI_API_KEY = 'YOUR_OPENAI_API_KEY';\n const ANTHROPIC_API_KEY = 'YOUR_ANTHROPIC_API_KEY';\n const MISTRAL_API_KEY = 'U4TD40GfA96d4txjFQzQSps2PXaxKYHC';\n const GOOGLE_API_KEY = 'AIzaSyB21Sv2aZEJ33TJ02dfLMx-PklP4ZVcG40';\n const COHERE_API_KEY = 'bAVVqL7U4wv2gmd4gbGBQwpVXDEGZqFm42czSg3a';\n const TOGETHER_API_KEY = '99ac6265846d3cb6a8f83e038605d01a4d1c6b8b80e4a64d3486460a89b85df3';\n const PERPLEXITY_API_KEY = 'YOUR_PERPLEXITY_API_KEY';\n const GROK_API_KEY = 'YOUR_XAI_API_KEY';\n \n // APIs Média (Vidéo/Avatar)\n const DID_API_KEY = 'YmVuc2FsZW1ib3VhbGVtQGdtYWlsLmNvbQ:XDqHY1ZQh9pWKHviJqMed';\n\n const PROVIDERS = {\n deepseek: {\n name: 'DeepSeek 🇨🇳',\n models: [\n { id: 'deepseek.deepseek-chat', name: 'DeepSeek Chat V3', strengths: ['Économique', 'Raisonnement', 'Code'], cost: 1 },\n { id: 'deepseek.deepseek-reasoner', name: 'DeepSeek R1 🧠', strengths: ['Raisonnement avancé', 'Math', 'Logique'], cost: 3 }\n ]\n },\n groq: {\n name: 'Groq ⚡',\n models: [\n { id: 'groq.llama-3.3-70b-versatile', name: 'Llama 3.3 70B', strengths: ['Puissant', 'Versatile', '131K ctx'], cost: 0 },\n { id: 'groq.llama-3.1-8b-instant', name: 'Llama 3.1 8B', strengths: ['Ultra-rapide', 'Gratuit'], cost: 0 },\n { id: 'groq.moonshotai/kimi-k2-instruct', name: 'Kimi K2 🌙', strengths: ['Moonshot AI', 'Raisonnement'], cost: 0 },\n { id: 'groq.qwen/qwen3-32b', name: 'Qwen3 32B', strengths: ['Alibaba', 'Multilingue', 'Code'], cost: 0 },\n { id: 'groq.meta-llama/llama-4-maverick-17b-128e-instruct', name: 'Llama 4 Maverick 🦙', strengths: ['Meta', 'Nouveau', '131K ctx'], cost: 0 },\n { id: 'groq.meta-llama/llama-4-scout-17b-16e-instruct', name: 'Llama 4 Scout 🦙', strengths: ['Meta', 'Exploration'], cost: 0 },\n { id: 'groq.openai/gpt-oss-120b', name: 'GPT-OSS 120B', strengths: ['OpenAI OSS', 'Très puissant'], cost: 0 },\n { id: 'groq.openai/gpt-oss-20b', name: 'GPT-OSS 20B', strengths: ['OpenAI OSS', 'Rapide'], cost: 0 },\n { id: 'groq.allam-2-7b', name: 'Allam 2 7B 🇸🇦', strengths: ['Arabe natif', 'SDAIA'], cost: 0 }\n ]\n },\n openrouter: {\n name: 'OpenRouter 🌐',\n models: [\n { id: 'openrouter.google/gemini-2.0-flash-exp:free', name: 'Gemini 2.0 Flash (Free)', strengths: ['Gratuit', 'Google', 'Rapide'], cost: 0 },\n { id: 'openrouter.meta-llama/llama-3.2-3b-instruct:free', name: 'Llama 3.2 3B (Free)', strengths: ['Gratuit', 'Meta', 'Compact'], cost: 0 },\n { id: 'openrouter.mistralai/mistral-7b-instruct:free', name: 'Mistral 7B (Free)', strengths: ['Gratuit', 'FR natif'], cost: 0 },\n { id: 'openrouter.deepseek/deepseek-chat', name: 'DeepSeek Chat 🇨🇳', strengths: ['Économique', 'Code', 'Raisonnement'], cost: 2 },\n { id: 'openrouter.deepseek/deepseek-r1', name: 'DeepSeek R1 🧠', strengths: ['Raisonnement avancé', 'Math'], cost: 5 },\n { id: 'openrouter.qwen/qwen-2.5-72b-instruct', name: 'Qwen 2.5 72B', strengths: ['Alibaba', 'Puissant'], cost: 10 },\n { id: 'openrouter.anthropic/claude-3.5-sonnet', name: 'Claude 3.5 Sonnet', strengths: ['Anthropic', 'Écriture'], cost: 15 },\n { id: 'openrouter.openai/gpt-4o-mini', name: 'GPT-4o Mini', strengths: ['OpenAI', 'Rapide'], cost: 5 },\n { id: 'openrouter.openai/gpt-4o', name: 'GPT-4o', strengths: ['OpenAI', 'Meilleur'], cost: 25 }\n ]\n },\n ollama: {\n name: 'Ollama 🏠 (lent)',\n models: [\n { id: 'ollama.llama3.2:3b', name: 'Llama 3.2 3B', strengths: ['Local', 'Privé', 'Rapide'], cost: 0 },\n { id: 'ollama.mistral:7b', name: 'Mistral 7B', strengths: ['Local', 'FR natif', 'Équilibré'], cost: 0 },\n { id: 'ollama.qwen2.5:3b', name: 'Qwen 2.5 3B', strengths: ['Local', 'Multilingue', 'Compact'], cost: 0 }\n ]\n },\n // ============ PROVIDERS PREMIUM (nécessitent clé API) ============\n openai: {\n name: 'OpenAI 🤖 (ChatGPT)',\n models: [\n { id: 'openai.gpt-4o', name: 'GPT-4o', strengths: ['Meilleur', 'Multimodal', '128K ctx'], cost: 25 },\n { id: 'openai.gpt-4o-mini', name: 'GPT-4o Mini', strengths: ['Rapide', 'Économique'], cost: 5 },\n { id: 'openai.gpt-4-turbo', name: 'GPT-4 Turbo', strengths: ['Puissant', '128K ctx'], cost: 30 },\n { id: 'openai.gpt-4', name: 'GPT-4', strengths: ['Original', 'Stable'], cost: 30 },\n { id: 'openai.gpt-3.5-turbo', name: 'GPT-3.5 Turbo', strengths: ['Économique', 'Rapide'], cost: 2 },\n { id: 'openai.o1-preview', name: 'O1 Preview 🧠', strengths: ['Raisonnement', 'Math', 'Code'], cost: 60 },\n { id: 'openai.o1-mini', name: 'O1 Mini 🧠', strengths: ['Raisonnement', 'Économique'], cost: 12 }\n ]\n },\n anthropic: {\n name: 'Anthropic 🎭 (Claude)',\n models: [\n { id: 'anthropic.claude-3-5-sonnet-20241022', name: 'Claude 3.5 Sonnet', strengths: ['Meilleur', 'Code', 'Écriture'], cost: 15 },\n { id: 'anthropic.claude-3-5-haiku-20241022', name: 'Claude 3.5 Haiku', strengths: ['Rapide', 'Économique'], cost: 4 },\n { id: 'anthropic.claude-3-opus-20240229', name: 'Claude 3 Opus', strengths: ['Très puissant', 'Analyse'], cost: 75 },\n { id: 'anthropic.claude-3-sonnet-20240229', name: 'Claude 3 Sonnet', strengths: ['Équilibré', 'Versatile'], cost: 15 },\n { id: 'anthropic.claude-3-haiku-20240307', name: 'Claude 3 Haiku', strengths: ['Ultra-rapide', 'Compact'], cost: 1 }\n ]\n },\n perplexity: {\n name: 'Perplexity 🔍',\n models: [\n { id: 'perplexity.llama-3.1-sonar-large-128k-online', name: 'Sonar Large Online', strengths: ['Recherche web', 'Temps réel', '128K'], cost: 5 },\n { id: 'perplexity.llama-3.1-sonar-small-128k-online', name: 'Sonar Small Online', strengths: ['Recherche web', 'Économique'], cost: 2 },\n { id: 'perplexity.llama-3.1-sonar-huge-128k-online', name: 'Sonar Huge Online', strengths: ['Le plus puissant', 'Recherche'], cost: 20 },\n { id: 'perplexity.llama-3.1-sonar-large-128k-chat', name: 'Sonar Large Chat', strengths: ['Sans recherche', 'Rapide'], cost: 3 },\n { id: 'perplexity.llama-3.1-sonar-small-128k-chat', name: 'Sonar Small Chat', strengths: ['Sans recherche', 'Économique'], cost: 1 }\n ]\n },\n grok: {\n name: 'Grok 🚀 (xAI)',\n models: [\n { id: 'grok.grok-beta', name: 'Grok Beta', strengths: ['Nouveau', 'Humour', 'Temps réel'], cost: 10 },\n { id: 'grok.grok-2-1212', name: 'Grok 2', strengths: ['Dernière version', 'Puissant'], cost: 15 },\n { id: 'grok.grok-2-vision-1212', name: 'Grok 2 Vision', strengths: ['Multimodal', 'Images'], cost: 20 }\n ]\n },\n mistral: {\n name: 'Mistral 🇫🇷',\n models: [\n { id: 'mistral.mistral-large-latest', name: 'Mistral Large', strengths: ['Le meilleur', 'FR natif', '128K'], cost: 12 },\n { id: 'mistral.mistral-medium-latest', name: 'Mistral Medium', strengths: ['Équilibré', 'Polyvalent'], cost: 8 },\n { id: 'mistral.mistral-small-latest', name: 'Mistral Small', strengths: ['Rapide', 'Économique'], cost: 3 },\n { id: 'mistral.open-mistral-nemo', name: 'Mistral Nemo', strengths: ['128K ctx', 'Multilingue'], cost: 2 },\n { id: 'mistral.codestral-latest', name: 'Codestral', strengths: ['Code', 'Programmation'], cost: 3 },\n { id: 'mistral.ministral-8b-latest', name: 'Ministral 8B', strengths: ['Compact', 'Edge'], cost: 1 },\n { id: 'mistral.ministral-3b-latest', name: 'Ministral 3B', strengths: ['Ultra-compact', 'Rapide'], cost: 0.5 },\n { id: 'mistral.pixtral-large-latest', name: 'Pixtral Large', strengths: ['Vision', 'Images'], cost: 15 }\n ]\n }\n };\n\n // NOTE: ⚡ = Groq ultra-rapide, 🌐 = OpenRouter (gratuit/payant), 🏠 = Local (lent sur VPS)\n\n const PRESETS = {\n openrouter: {\n name: 'OpenRouter Free',\n experts: [\n { provider: 'openrouter', model: 'openrouter.google/gemini-2.0-flash-exp:free' },\n { provider: 'openrouter', model: 'openrouter.meta-llama/llama-3.2-3b-instruct:free' },\n { provider: 'openrouter', model: 'openrouter.mistralai/mistral-7b-instruct:free' }\n ],\n chairman: { provider: 'openrouter', model: 'openrouter.google/gemini-2.0-flash-exp:free' }\n },\n fast: {\n name: 'Groq Ultra Rapide',\n experts: [\n { provider: 'groq', model: 'groq.llama-3.3-70b-versatile' },\n { provider: 'groq', model: 'groq.qwen/qwen3-32b' },\n { provider: 'groq', model: 'groq.moonshotai/kimi-k2-instruct' }\n ],\n chairman: { provider: 'groq', model: 'groq.llama-3.3-70b-versatile' }\n },\n diversified: {\n name: 'Diversifié',\n experts: [\n { provider: 'groq', model: 'groq.llama-3.3-70b-versatile' },\n { provider: 'groq', model: 'groq.moonshotai/kimi-k2-instruct' },\n { provider: 'deepseek', model: 'deepseek.deepseek-chat' }\n ],\n chairman: { provider: 'deepseek', model: 'deepseek.deepseek-reasoner' }\n },\n deepseek: {\n name: 'DeepSeek Power 🇨🇳',\n experts: [\n { provider: 'deepseek', model: 'deepseek.deepseek-chat' },\n { provider: 'deepseek', model: 'deepseek.deepseek-reasoner' },\n { provider: 'groq', model: 'groq.llama-3.3-70b-versatile' }\n ],\n chairman: { provider: 'deepseek', model: 'deepseek.deepseek-reasoner' }\n },\n llama4: {\n name: 'Llama 4 🦙',\n experts: [\n { provider: 'groq', model: 'groq.meta-llama/llama-4-maverick-17b-128e-instruct' },\n { provider: 'groq', model: 'groq.meta-llama/llama-4-scout-17b-16e-instruct' },\n { provider: 'groq', model: 'groq.llama-3.3-70b-versatile' }\n ],\n chairman: { provider: 'groq', model: 'groq.meta-llama/llama-4-maverick-17b-128e-instruct' }\n },\n hybrid: {\n name: 'Hybride',\n experts: [\n { provider: 'groq', model: 'groq.llama-3.3-70b-versatile' },\n { provider: 'openrouter', model: 'openrouter.google/gemini-2.0-flash-exp:free' },\n { provider: 'groq', model: 'groq.qwen/qwen3-32b' }\n ],\n chairman: { provider: 'groq', model: 'groq.moonshotai/kimi-k2-instruct' }\n },\n local: {\n name: '100% Local',\n experts: [\n { provider: 'ollama', model: 'ollama.llama3.2:3b' },\n { provider: 'ollama', model: 'ollama.mistral:7b' },\n { provider: 'ollama', model: 'ollama.qwen2.5:3b' }\n ],\n chairman: { provider: 'ollama', model: 'ollama.mistral:7b' }\n }\n };\n\n let currentOpinions = {};\n\n // ============================================\n // Initialization\n // ============================================\n document.addEventListener('DOMContentLoaded', () => {\n // Load theme\n const savedTheme = localStorage.getItem('council-theme') || 'dark';\n document.documentElement.setAttribute('data-theme', savedTheme);\n updateThemeIcon(savedTheme);\n\n // Initialize all expert selects\n [1, 2, 3, 'Chairman'].forEach(num => {\n onProviderChange(num);\n });\n\n updateEstimate();\n });\n\n // ============================================\n // Theme Toggle\n // ============================================\n\n function updateThemeIcon(theme) {\n const icon = document.getElementById('theme-icon');\n icon.className = theme === 'dark' ? 'fas fa-moon' : 'fas fa-sun';\n }\n\n // ============================================\n // Provider/Model Management\n // ============================================\n function onProviderChange(expertNum) {\n const providerSelect = document.getElementById(`provider${expertNum}`);\n const modelSelect = document.getElementById(`model${expertNum}`);\n const strengthsBox = document.getElementById(`strengths${expertNum}`);\n \n const provider = providerSelect.value;\n const models = PROVIDERS[provider].models;\n\n // Populate models\n modelSelect.innerHTML = models.map(m => \n `<option value=\"${m.id}\">${m.name}</option>`\n ).join('');\n\n // Update strengths\n updateStrengths(expertNum);\n \n // Add change listener to model\n modelSelect.onchange = () => updateStrengths(expertNum);\n\n updateEstimate();\n }\n\n function updateStrengths(expertNum) {\n const providerSelect = document.getElementById(`provider${expertNum}`);\n const modelSelect = document.getElementById(`model${expertNum}`);\n const strengthsBox = document.getElementById(`strengths${expertNum}`);\n\n const provider = providerSelect.value;\n const modelId = modelSelect.value;\n const model = PROVIDERS[provider].models.find(m => m.id === modelId);\n\n if (model) {\n strengthsBox.textContent = `💪 ${model.strengths.join(', ')}`;\n }\n\n updateEstimate();\n }\n\n // ============================================\n // Presets\n // ============================================\n function applyPreset(presetName) {\n const preset = PRESETS[presetName];\n \n // Update presets UI\n document.querySelectorAll('.preset-card').forEach(card => {\n card.classList.remove('active');\n });\n event.currentTarget.classList.add('active');\n\n // Apply expert configs\n preset.experts.forEach((exp, idx) => {\n const num = idx + 1;\n document.getElementById(`provider${num}`).value = exp.provider;\n onProviderChange(num);\n document.getElementById(`model${num}`).value = exp.model;\n updateStrengths(num);\n });\n\n // Apply chairman config\n document.getElementById('providerChairman').value = preset.chairman.provider;\n onProviderChange('Chairman');\n document.getElementById('modelChairman').value = preset.chairman.model;\n updateStrengths('Chairman');\n\n updateEstimate();\n }\n\n // ============================================\n // Estimation\n // ============================================\n function updateEstimate() {\n let totalCost = 0;\n let fastestTime = true;\n\n [1, 2, 3, 'Chairman'].forEach(num => {\n const provider = document.getElementById(`provider${num}`).value;\n const modelId = document.getElementById(`model${num}`).value;\n const model = PROVIDERS[provider]?.models.find(m => m.id === modelId);\n \n if (model) {\n totalCost += model.cost;\n if (provider !== 'groq') fastestTime = false;\n }\n });\n\n const enableReview = document.getElementById('enableReview').checked;\n if (enableReview) {\n totalCost *= 1.5;\n }\n\n // Update UI\n document.getElementById('costEstimate').textContent = totalCost === 0 ? 'Gratuit!' : `~${Math.round(totalCost)} DZD`;\n document.getElementById('timeEstimate').textContent = fastestTime ? '~5s' : (enableReview ? '~30s' : '~15s');\n \n // Show warning for expensive configs\n document.getElementById('warningBox').style.display = totalCost > 100 ? 'block' : 'none';\n }\n\n // ============================================\n // Execute Council\n // ============================================\n async function executeCouncil() {\n const query = document.getElementById('queryInput').value.trim();\n if (!query) {\n showError('Veuillez entrer une question');\n return;\n }\n\n // Get configuration\n const experts = [1, 2, 3].map(num => ({\n provider: document.getElementById(`provider${num}`).value,\n model: document.getElementById(`model${num}`).value\n }));\n \n const chairman = {\n provider: document.getElementById('providerChairman').value,\n model: document.getElementById('modelChairman').value\n };\n\n const enableReview = document.getElementById('enableReview').checked;\n\n // Show loading\n document.getElementById('loadingBox').style.display = 'block';\n document.getElementById('resultsSection').style.display = 'none';\n document.getElementById('submitBtn').disabled = true;\n\n const startTime = Date.now();\n\n try {\n // Call each expert in parallel\n document.getElementById('loadingStatus').textContent = 'Les experts analysent votre question...';\n \n // Detect language of input for instruction\n const langInstruction = '\\n\\n[IMPORTANT: Respond in the SAME language as the question above. If the question is in French, answer in French. If in English, answer in English. If in Arabic, answer in Arabic.]';\n \n const expertPromises = experts.map((exp, idx) => \n callLLM(exp.model, query + langInstruction, `Expert ${idx + 1}`)\n );\n\n const expertResponses = await Promise.all(expertPromises);\n\n // Store opinions\n currentOpinions = {};\n expertResponses.forEach((response, idx) => {\n currentOpinions[`expert${idx + 1}`] = response;\n });\n\n // Chairman synthesis\n document.getElementById('loadingStatus').textContent = 'Le Chairman synthétise les réponses...';\n \n const synthesisPrompt = `You are the Chairman of an AI expert council. \nHere is the question asked: \"${query}\"\n\nHere are the expert opinions:\n\nExpert 1 (${getModelName(experts[0].model)}):\n${expertResponses[0]}\n\nExpert 2 (${getModelName(experts[1].model)}):\n${expertResponses[1]}\n\nExpert 3 (${getModelName(experts[2].model)}):\n${expertResponses[2]}\n\nAs Chairman, provide a comprehensive synthesis that:\n1. Identifies points of consensus\n2. Notes unique perspectives from each expert\n3. Proposes a balanced and complete final answer\n\nCRITICAL: You MUST respond in the SAME LANGUAGE as the original question. If the question is in French, respond entirely in French. If in English, respond in English. If in Arabic, respond in Arabic. Match the language exactly.`;\n\n const finalResponse = await callLLM(chairman.model, synthesisPrompt, 'Chairman');\n\n // Calculate stats\n const execTime = ((Date.now() - startTime) / 1000).toFixed(1);\n\n // Display results\n displayResults({\n finalResponse,\n chairman,\n experts,\n execTime,\n opinions: currentOpinions\n });\n\n } catch (error) {\n showError(`Erreur: ${error.message}`);\n } finally {\n document.getElementById('loadingBox').style.display = 'none';\n document.getElementById('submitBtn').disabled = false;\n }\n }\n\n async function callLLM(model, prompt, role) {\n try {\n // Check if it's an Ollama model\n if (model.startsWith('ollama.')) {\n return await callOllama(model, prompt, role);\n }\n\n // Check if it's an OpenRouter model\n if (model.startsWith('openrouter.')) {\n return await callOpenRouter(model, prompt, role);\n }\n\n // Check if it's a Groq model - call directly\n if (model.startsWith('groq.')) {\n return await callGroq(model, prompt, role);\n }\n\n // Check if it's a DeepSeek model - call directly\n if (model.startsWith('deepseek.')) {\n return await callDeepSeek(model, prompt, role);\n }\n\n // Check if it's an OpenAI/ChatGPT model\n if (model.startsWith('openai.')) {\n return await callOpenAI(model, prompt, role);\n }\n\n // Check if it's an Anthropic/Claude model\n if (model.startsWith('anthropic.')) {\n return await callAnthropic(model, prompt, role);\n }\n\n // Check if it's a Perplexity model\n if (model.startsWith('perplexity.')) {\n return await callPerplexity(model, prompt, role);\n }\n\n // Check if it's a Grok/xAI model\n if (model.startsWith('grok.')) {\n return await callGrok(model, prompt, role);\n }\n\n // Check if it's a Mistral model\n if (model.startsWith('mistral.')) {\n return await callMistral(model, prompt, role);\n }\n\n // Standard API call for other providers (fallback)\n const systemPrompt = 'You MUST respond in the same language as the user input. If the user writes in French, respond in French. If in English, respond in English. Match the language exactly.';\n const response = await fetch('/api/llm/chat', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n model: model,\n messages: [\n { role: 'system', content: systemPrompt },\n { role: 'user', content: prompt }\n ],\n temperature: 0.7,\n max_tokens: 2000\n })\n });\n\n const data = await response.json();\n \n if (!response.ok || !data.success) {\n let errorMsg = 'Erreur inconnue';\n if (typeof data.detail === 'string') {\n errorMsg = data.detail;\n } else if (typeof data.error === 'string') {\n errorMsg = data.error;\n } else if (data.detail && typeof data.detail === 'object') {\n errorMsg = JSON.stringify(data.detail);\n } else if (data.error && typeof data.error === 'object') {\n errorMsg = JSON.stringify(data.error);\n }\n throw new Error(`${role}: ${errorMsg}`);\n }\n\n return data.answer || data.response || 'Réponse vide';\n } catch (fetchError) {\n if (fetchError.message.includes(role)) {\n throw fetchError;\n }\n throw new Error(`${role}: ${fetchError.message}`);\n }\n }\n\n async function callOllama(model, prompt, role) {\n // Extract actual model name (e.g., \"ollama.mistral:7b\" -> \"mistral:7b\")\n const ollamaModel = model.replace('ollama.', '');\n const systemPrompt = 'You MUST respond in the same language as the user input. Match the language exactly.';\n \n try {\n const response = await fetch('/ollama/api/generate', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n model: ollamaModel,\n prompt: prompt,\n system: systemPrompt,\n stream: false\n })\n });\n\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}`);\n }\n\n const data = await response.json();\n return data.response || 'Réponse vide';\n } catch (error) {\n throw new Error(`${role} (Ollama): ${error.message}`);\n }\n }\n\n async function callOpenRouter(model, prompt, role) {\n // Extract actual model name (e.g., \"openrouter.deepseek/deepseek-chat\" -> \"deepseek/deepseek-chat\")\n const openrouterModel = model.replace('openrouter.', '');\n \n try {\n const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${OPENROUTER_API_KEY}`,\n 'HTTP-Referer': window.location.href,\n 'X-Title': 'IAFactory Council'\n },\n body: JSON.stringify({\n model: openrouterModel,\n messages: [\n { role: 'system', content: 'You MUST respond in the same language as the user input. Match the language exactly.' },\n { role: 'user', content: prompt }\n ],\n max_tokens: 2000,\n temperature: 0.7\n })\n });\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n const errorMsg = errorData.error?.message || `HTTP ${response.status}`;\n throw new Error(errorMsg);\n }\n\n const data = await response.json();\n return data.choices?.[0]?.message?.content || 'Réponse vide';\n } catch (error) {\n throw new Error(`${role} (OpenRouter): ${error.message}`);\n }\n }\n\n async function callGroq(model, prompt, role) {\n // Extract actual model name (e.g., \"groq.llama-3.3-70b-versatile\" -> \"llama-3.3-70b-versatile\")\n const groqModel = model.replace('groq.', '');\n \n try {\n const response = await fetch('https://api.groq.com/openai/v1/chat/completions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${GROQ_API_KEY}`\n },\n body: JSON.stringify({\n model: groqModel,\n messages: [\n { role: 'system', content: 'You MUST respond in the same language as the user input. Match the language exactly.' },\n { role: 'user', content: prompt }\n ],\n max_tokens: 2000,\n temperature: 0.7\n })\n });\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n const errorMsg = errorData.error?.message || `HTTP ${response.status}`;\n throw new Error(errorMsg);\n }\n\n const data = await response.json();\n return data.choices?.[0]?.message?.content || 'Réponse vide';\n } catch (error) {\n throw new Error(`${role} (Groq): ${error.message}`);\n }\n }\n\n async function callDeepSeek(model, prompt, role) {\n // Extract actual model name (e.g., \"deepseek.deepseek-chat\" -> \"deepseek-chat\")\n const deepseekModel = model.replace('deepseek.', '');\n \n try {\n const response = await fetch('https://api.deepseek.com/v1/chat/completions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${DEEPSEEK_API_KEY}`\n },\n body: JSON.stringify({\n model: deepseekModel,\n messages: [\n { role: 'system', content: 'You MUST respond in the same language as the user input. If the user writes in French, respond in French. If in English, respond in English. Never respond in Chinese unless the user writes in Chinese.' },\n { role: 'user', content: prompt }\n ],\n max_tokens: 2000,\n temperature: 0.7\n })\n });\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n const errorMsg = errorData.error?.message || `HTTP ${response.status}`;\n throw new Error(errorMsg);\n }\n\n const data = await response.json();\n return data.choices?.[0]?.message?.content || 'Réponse vide';\n } catch (error) {\n throw new Error(`${role} (DeepSeek): ${error.message}`);\n }\n }\n\n // ============================================\n // OpenAI / ChatGPT API\n // ============================================\n async function callOpenAI(model, prompt, role) {\n if (!OPENAI_API_KEY) {\n throw new Error('Clé API OpenAI non configurée. Ajoutez votre clé dans OPENAI_API_KEY');\n }\n const openaiModel = model.replace('openai.', '');\n \n try {\n const response = await fetch('https://api.openai.com/v1/chat/completions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${OPENAI_API_KEY}`\n },\n body: JSON.stringify({\n model: openaiModel,\n messages: [\n { role: 'system', content: 'You MUST respond in the same language as the user input. Match the language exactly.' },\n { role: 'user', content: prompt }\n ],\n max_tokens: 2000,\n temperature: 0.7\n })\n });\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n const errorMsg = errorData.error?.message || `HTTP ${response.status}`;\n throw new Error(errorMsg);\n }\n\n const data = await response.json();\n return data.choices?.[0]?.message?.content || 'Réponse vide';\n } catch (error) {\n throw new Error(`${role} (OpenAI): ${error.message}`);\n }\n }\n\n // ============================================\n // Anthropic / Claude API\n // ============================================\n async function callAnthropic(model, prompt, role) {\n if (!ANTHROPIC_API_KEY) {\n throw new Error('Clé API Anthropic non configurée. Ajoutez votre clé dans ANTHROPIC_API_KEY');\n }\n const claudeModel = model.replace('anthropic.', '');\n \n try {\n const response = await fetch('https://api.anthropic.com/v1/messages', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'x-api-key': ANTHROPIC_API_KEY,\n 'anthropic-version': '2023-06-01',\n 'anthropic-dangerous-direct-browser-access': 'true'\n },\n body: JSON.stringify({\n model: claudeModel,\n max_tokens: 2000,\n system: 'You MUST respond in the same language as the user input. Match the language exactly.',\n messages: [\n { role: 'user', content: prompt }\n ]\n })\n });\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n const errorMsg = errorData.error?.message || `HTTP ${response.status}`;\n throw new Error(errorMsg);\n }\n\n const data = await response.json();\n return data.content?.[0]?.text || 'Réponse vide';\n } catch (error) {\n throw new Error(`${role} (Claude): ${error.message}`);\n }\n }\n\n // ============================================\n // Perplexity API\n // ============================================\n async function callPerplexity(model, prompt, role) {\n if (!PERPLEXITY_API_KEY) {\n throw new Error('Clé API Perplexity non configurée. Ajoutez votre clé dans PERPLEXITY_API_KEY');\n }\n const perplexityModel = model.replace('perplexity.', '');\n \n try {\n const response = await fetch('https://api.perplexity.ai/chat/completions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${PERPLEXITY_API_KEY}`\n },\n body: JSON.stringify({\n model: perplexityModel,\n messages: [\n { role: 'system', content: 'You MUST respond in the same language as the user input. Match the language exactly.' },\n { role: 'user', content: prompt }\n ],\n max_tokens: 2000,\n temperature: 0.7\n })\n });\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n const errorMsg = errorData.error?.message || `HTTP ${response.status}`;\n throw new Error(errorMsg);\n }\n\n const data = await response.json();\n return data.choices?.[0]?.message?.content || 'Réponse vide';\n } catch (error) {\n throw new Error(`${role} (Perplexity): ${error.message}`);\n }\n }\n\n // ============================================\n // Grok / xAI API\n // ============================================\n async function callGrok(model, prompt, role) {\n if (!GROK_API_KEY) {\n throw new Error('Clé API Grok/xAI non configurée. Ajoutez votre clé dans GROK_API_KEY');\n }\n const grokModel = model.replace('grok.', '');\n \n try {\n const response = await fetch('https://api.x.ai/v1/chat/completions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${GROK_API_KEY}`\n },\n body: JSON.stringify({\n model: grokModel,\n messages: [\n { role: 'system', content: 'You MUST respond in the same language as the user input. Match the language exactly.' },\n { role: 'user', content: prompt }\n ],\n max_tokens: 2000,\n temperature: 0.7\n })\n });\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n const errorMsg = errorData.error?.message || `HTTP ${response.status}`;\n throw new Error(errorMsg);\n }\n\n const data = await response.json();\n return data.choices?.[0]?.message?.content || 'Réponse vide';\n } catch (error) {\n throw new Error(`${role} (Grok): ${error.message}`);\n }\n }\n\n // ============================================\n // Mistral API\n // ============================================\n async function callMistral(model, prompt, role) {\n if (!MISTRAL_API_KEY) {\n throw new Error('Clé API Mistral non configurée. Ajoutez votre clé dans MISTRAL_API_KEY');\n }\n const mistralModel = model.replace('mistral.', '');\n \n try {\n const response = await fetch('https://api.mistral.ai/v1/chat/completions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${MISTRAL_API_KEY}`\n },\n body: JSON.stringify({\n model: mistralModel,\n messages: [\n { role: 'system', content: 'You MUST respond in the same language as the user input. Match the language exactly.' },\n { role: 'user', content: prompt }\n ],\n max_tokens: 2000,\n temperature: 0.7\n })\n });\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n const errorMsg = errorData.error?.message || `HTTP ${response.status}`;\n throw new Error(errorMsg);\n }\n\n const data = await response.json();\n return data.choices?.[0]?.message?.content || 'Réponse vide';\n } catch (error) {\n throw new Error(`${role} (Mistral): ${error.message}`);\n }\n }\n\n function getModelName(modelId) {\n for (const provider of Object.values(PROVIDERS)) {\n const model = provider.models.find(m => m.id === modelId);\n if (model) return model.name;\n }\n return modelId;\n }\n\n // ============================================\n // Display Results\n // ============================================\n function displayResults(data) {\n document.getElementById('resultsSection').style.display = 'block';\n document.getElementById('finalResponse').textContent = data.finalResponse;\n document.getElementById('chairmanName').textContent = `(${getModelName(data.chairman.model)})`;\n document.getElementById('execTime').innerHTML = `<i class=\"fas fa-clock\"></i> ${data.execTime}s`;\n document.getElementById('actualCost').innerHTML = `<i class=\"fas fa-coins\"></i> Estimé`;\n document.getElementById('expertsCount').innerHTML = `<i class=\"fas fa-users\"></i> 3 experts + Chairman`;\n\n // Build opinion tabs\n const tabsHTML = data.experts.map((exp, idx) => `\n <button class=\"opinion-tab ${idx === 0 ? 'active' : ''}\" \n onclick=\"showOpinion('expert${idx + 1}', this)\">\n Expert ${idx + 1} (${getModelName(exp.model)})\n </button>\n `).join('');\n\n document.getElementById('opinionTabs').innerHTML = tabsHTML;\n showOpinion('expert1', document.querySelector('.opinion-tab'));\n\n // Scroll to results\n document.getElementById('resultsSection').scrollIntoView({ behavior: 'smooth' });\n }\n\n function showOpinion(expertKey, tabElement) {\n // Update tabs\n document.querySelectorAll('.opinion-tab').forEach(tab => tab.classList.remove('active'));\n tabElement.classList.add('active');\n\n // Show content\n document.getElementById('opinionContent').textContent = currentOpinions[expertKey] || 'Opinion non disponible';\n }\n\n // ============================================\n // Error Handling\n // ============================================\n function showError(message) {\n const toast = document.getElementById('errorToast');\n document.getElementById('errorMessage').textContent = message;\n toast.classList.add('visible');\n\n setTimeout(() => {\n toast.classList.remove('visible');\n }, 5000);\n }\n\n // Listen for review checkbox\n document.getElementById('enableReview').addEventListener('change', updateEstimate);\n </script>\n<!-- ========== CHATBOT HELP FLOTTANT - À inclure avant \n<div id=\"cb-help\" style=\"position: fixed; bottom: 24px; right: 24px; z-index: 9999;\">\n <button onclick=\"document.getElementById('cb-fr').style.display = document.getElementById('cb-fr').style.display === 'block' ? 'none' : 'block'\" style=\"width: 60px; height: 60px; border-radius: 50%; background: linear-gradient(135deg, #00a651, #00d66a); border: none; cursor: pointer; color: white; font-size: 24px;\">?</button>\n <div id=\"cb-fr\" style=\"display: none; position: fixed; bottom: 100px; right: 24px; width: 380px; height: 600px; border-radius: 16px; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); background: white; overflow: hidden;\">\n <iframe src=\"../chatbot-ia/index.html\" style=\"width: 100%; height: 100%; border: none;\"></iframe>\n </div>\n</div>\n <script src=\"/apps/shared/iafactory-unified.js\"></script>\n <div data-iaf-footer></div>\n\\1 ========== -->\n</div>\n\n<script>\n// ============================================\n \n if (!response.ok) throw new Error('Erreur API');\n \n const data = await response.json();\n const botResponse = data.choices?.[0]?.message?.content || 'Désolé, je n\\'ai pas pu répondre.';\n \n } catch (error) {\n const messagesContainer = document.getElementById('helpMessages');\n if (messagesContainer && messagesContainer.lastChild) {\n messagesContainer.removeChild(messagesContainer.lastChild);\n }\n }\n}\n\n const messagesContainer = document.getElementById('helpMessages');\n if (!messagesContainer) return;\n const messageDiv = document.createElement('div');\n messageDiv.className = `help-message help-${sender}`;\n \n const avatar = sender === 'bot'\n ? `<div style=\"width: 32px; height: 32px; background: linear-gradient(135deg, #00843D 0%, #2ecc71 100%); color: white; border-radius: 8px; display: flex; align-items: center; justify-content: center; font-weight: bold; font-size: 11px;\">DZ</div>`\n : '👤';\n\n messageDiv.innerHTML = `\n <div class=\"help-avatar\">${avatar}</div>\n `;\n \n messagesContainer.appendChild(messageDiv);\n messagesContainer.scrollTop = messagesContainer.scrollHeight;\n}\n\n}\n</script>\n\n<style>\n position: fixed;\n bottom: 24px;\n right: 24px;\n z-index: 9999;\n display: flex;\n align-items: center;\n gap: 12px;\n}\n\n.help-label {\n background: #00A651;\n color: white;\n padding: 10px 16px;\n border-radius: 20px;\n font-size: 14px;\n font-weight: 500;\n white-space: nowrap;\n box-shadow: 0 4px 15px rgba(0, 166, 81, 0.3);\n opacity: 0;\n transform: translateX(20px);\n transition: all 0.3s ease;\n pointer-events: none;\n}\n\n opacity: 1;\n transform: translateX(0);\n}\n\n.help-btn {\n width: 60px;\n height: 60px;\n border-radius: 50%;\n background: #00A651;\n border: none;\n cursor: pointer;\n font-size: 28px;\n box-shadow: 0 4px 20px rgba(0, 166, 81, 0.4);\n transition: all 0.3s;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.help-btn:hover {\n transform: scale(1.1);\n box-shadow: 0 6px 30px rgba(0, 166, 81, 0.6);\n}\n\n.help-window {\n position: absolute;\n bottom: 80px;\n right: 0;\n width: 340px;\n height: 480px;\n background: var(--bg-card, #0f172a);\n border: 1px solid var(--border-color, rgba(255,255,255,0.12));\n border-radius: 16px;\n box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);\n display: none;\n flex-direction: column;\n overflow: hidden;\n}\n\n.help-window.open {\n display: flex;\n}\n\n.help-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 16px;\n background: #00A651;\n color: white;\n}\n\n.help-header h3 {\n margin: 0;\n font-size: 18px;\n}\n\n.close-help-btn {\n background: none;\n border: none;\n color: white;\n font-size: 24px;\n cursor: pointer;\n}\n\n.help-messages {\n flex: 1;\n overflow-y: auto;\n padding: 16px;\n display: flex;\n flex-direction: column;\n gap: 12px;\n}\n\n.help-message {\n display: flex;\n gap: 8px;\n align-items: flex-start;\n}\n\n.help-message.help-user {\n flex-direction: row-reverse;\n}\n\n.help-avatar {\n width: 32px;\n height: 32px;\n border-radius: 50%;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 16px;\n flex-shrink: 0;\n}\n\n max-width: 75%;\n padding: 10px 14px;\n border-radius: 12px;\n font-size: 14px;\n line-height: 1.4;\n}\n\n background: var(--border-color, rgba(255,255,255,0.1));\n color: var(--text-primary, #f8fafc);\n border-bottom-left-radius: 4px;\n}\n\n background: #00A651;\n color: white;\n border-bottom-right-radius: 4px;\n}\n\n.help-input {\n display: flex;\n padding: 12px;\n gap: 8px;\n border-top: 1px solid var(--border-color, rgba(255,255,255,0.12));\n background: var(--bg-card, #0f172a);\n}\n\n.help-input input {\n flex: 1;\n padding: 10px 14px;\n border: 1px solid var(--border-color, rgba(255,255,255,0.12));\n border-radius: 20px;\n background: var(--input-bg, #1e293b);\n color: var(--input-text, #f8fafc);\n font-size: 14px;\n}\n\n.help-input input:focus {\n outline: none;\n border-color: #00A651;\n}\n\n.help-send-btn {\n width: 40px;\n height: 40px;\n border-radius: 50%;\n background: #00A651;\n border: none;\n color: white;\n font-size: 18px;\n cursor: pointer;\n transition: all 0.2s;\n}\n\n.help-send-btn:hover {\n background: #008c44;\n}\n\n@media (max-width: 768px) {\n bottom: 20px;\n right: 15px;\n }\n .help-window {\n width: calc(100vw - 30px);\n height: 60vh;\n bottom: 80px;\n right: -5px;\n }\n .help-btn {\n width: 56px;\n height: 56px;\n font-size: 24px;\n }\n}\n</style>\n\n\n<div id=\"cb-help\" style=\"position: fixed; bottom: 24px; right: 24px; z-index: 9999;\">\n <button onclick=\"document.getElementById('cb-fr').style.display = document.getElementById('cb-fr').style.display === 'block' ? 'none' : 'block'\" style=\"width: 60px; height: 60px; border-radius: 50%; background: linear-gradient(135deg, #00a651, #00d66a); border: none; cursor: pointer; color: white; font-size: 24px;\">?</button>\n <div id=\"cb-fr\" style=\"display: none; position: fixed; bottom: 100px; right: 24px; width: 380px; height: 600px; border-radius: 16px; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); background: white; overflow: hidden;\">\n <iframe src=\"../chatbot-ia/index.html\" style=\"width: 100%; height: 100%; border: none;\"></iframe>\n </div>\n</div>\n\n <div style=\"display:flex;padding:8px;gap:4px;background:rgba(0,0,0,0.2);\">\n <button class=\"iaf-chatbot-mode active\" id=\"iaf-mode-chat\" onclick=\"IAFactory.setChatMode('chat')\" style=\"flex:1;padding:8px;border:none;background:var(--accent,#00a651);color:white;cursor:pointer;border-radius:8px;font-size:12px;\" data-i18n=\"chatbotModeChat\">Chat IA</button>\n <button class=\"iaf-chatbot-mode\" id=\"iaf-mode-rag\" onclick=\"IAFactory.setChatMode('rag')\" style=\"flex:1;padding:8px;border:none;background:transparent;color:inherit;cursor:pointer;border-radius:8px;font-size:12px;opacity:0.6;\" data-i18n=\"chatbotModeRag\">Recherche</button>\n <button class=\"iaf-chatbot-mode\" id=\"iaf-mode-support\" onclick=\"IAFactory.setChatMode('support')\" style=\"flex:1;padding:8px;border:none;background:transparent;color:inherit;cursor:pointer;border-radius:8px;font-size:12px;opacity:0.6;\" data-i18n=\"chatbotModeSupport\">Support</button>\n </div>\n <div class=\"iaf-chatbot-messages\" id=\"iaf-chatbot-messages\" style=\"flex:1;overflow-y:auto;padding:16px;\">\n <div style=\"display:flex;gap:8px;margin-bottom:12px;\">\n <div style=\"width:32px;height:32px;background:linear-gradient(135deg,#00a651,#00d66a);color:white;border-radius:8px;display:flex;align-items:center;justify-content:center;font-weight:bold;font-size:11px;flex-shrink:0;\">DZ</div>\n <div style=\"background:rgba(0,0,0,0.2);padding:10px 14px;border-radius:12px;max-width:80%;font-size:14px;line-height:1.4;\" data-i18n=\"chatbotWelcome\" data-i18n-html>👋 Bonjour ! Je suis <strong>Dzir IA</strong>. Comment puis-je vous aider ?</div>\n </div>\n </div>\n <div style=\"display:flex;padding:12px;gap:8px;border-top:1px solid rgba(255,255,255,0.1);\">\n <input type=\"text\" id=\"iaf-chatbot-input\" placeholder=\"Tapez votre message...\" data-i18n-placeholder=\"chatbotPlaceholder\" onkeypress=\"if(event.key==='Enter')IAFactory.sendChatMessage()\" style=\"flex:1;padding:10px 14px;border:1px solid rgba(255,255,255,0.1);border-radius:20px;background:rgba(0,0,0,0.2);color:inherit;outline:none;font-size:14px;\">\n <button onclick=\"IAFactory.sendChatMessage()\" style=\"width:40px;height:40px;border-radius:50%;border:none;background:#00a651;color:white;cursor:pointer;font-size:16px;\">➤</button>\n </div>\n </div>\n\n<button class=\"iaf-chatbot-btn\" onclick=\"IAFactory.toggleChatbot()\" title=\"Aide\" aria-label=\"Aide\">💬</button>\n</body>\n</html>\n"}, "6e3e559c95f7add9ca35fe4046b933a73195120f": {"t": 1785749290.1823995, "n": 87686, "h": "f4c571a3f32cc4a8", "c": "<!DOCTYPE html>\n<html lang=\"fr\" data-theme=\"dark\" data-iaf-auto-init>\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Build Your Council - IA Factory Algeria</title>\n <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n <link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap\" rel=\"stylesheet\">\n <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css\">\n <style>\n :root {\n /* Dark Mode (default) - Aligné avec Landing Page */\n --bg-primary: #020617;\n --bg-secondary: #020617;\n --bg-card: #020617;\n --bg-card-hover: #0a0f1a;\n --border-color: rgba(255, 255, 255, 0.12);\n --text-primary: #f8fafc;\n --text-secondary: rgba(248, 250, 252, 0.75);\n --text-muted: rgba(248, 250, 252, 0.5);\n --accent-primary: #00a651;\n --accent-secondary: #00c95d;\n --accent-gradient: linear-gradient(135deg, #00a651, #008c45);\n --success: #10b981;\n --success-bg: rgba(16, 185, 129, 0.1);\n --warning: #f59e0b;\n --warning-bg: rgba(245, 158, 11, 0.1);\n --error: #ef4444;\n --input-bg: #0f172a;\n --input-border: rgba(255, 255, 255, 0.15);\n --input-text: #f8fafc;\n --input-placeholder: rgba(248, 250, 252, 0.5);\n }\n\n [data-theme=\"light\"] {\n --bg-primary: #f7f5f0;\n --bg-secondary: #f7f5f0;\n --bg-card: #f7f5f0;\n --bg-card-hover: #efe9df;\n --border-color: rgba(0, 0, 0, 0.08);\n --text-primary: #0f172a;\n --text-secondary: rgba(15, 23, 42, 0.7);\n --text-muted: rgba(15, 23, 42, 0.5);\n --accent-primary: #00a651;\n --accent-secondary: #008c45;\n --accent-gradient: linear-gradient(135deg, #00a651, #008c45);\n --success-bg: rgba(16, 185, 129, 0.1);\n --warning-bg: rgba(245, 158, 11, 0.1);\n --input-bg: #ffffff;\n --input-border: rgba(0, 0, 0, 0.12);\n --input-text: #0f172a;\n --input-placeholder: rgba(15, 23, 42, 0.5);\n }\n\n * {\n font-family: 'Inter', sans-serif;\n margin: 0;\n padding: 0;\n box-sizing: border-box;\n }\n\n body {\n background: var(--bg-primary);\n color: var(--text-primary);\n min-height: 100vh;\n transition: background 0.3s ease, color 0.3s ease;\n }\n\n /* Header */\n .main-header {\n background: var(--bg-secondary);\n border-bottom: 1px solid var(--border-color);\n padding: 1rem 2rem;\n display: flex;\n justify-content: space-between;\n align-items: center;\n position: sticky;\n top: 0;\n z-index: 100;\n }\n\n .logo {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n }\n\n .logo-icon {\n width: 40px;\n height: 40px;\n background: var(--accent-gradient);\n border-radius: 10px;\n display: flex;\n align-items: center;\n justify-content: center;\n color: white;\n font-size: 1.25rem;\n }\n\n .logo h1 {\n font-size: 1.25rem;\n font-weight: 700;\n background: var(--accent-gradient);\n -webkit-background-clip: text;\n -webkit-text-fill-color: transparent;\n }\n\n .header-actions {\n display: flex;\n align-items: center;\n gap: 1rem;\n }\n\n .theme-toggle {\n background: var(--bg-card);\n border: 1px solid var(--border-color);\n color: var(--text-primary);\n padding: 0.5rem 0.75rem;\n border-radius: 8px;\n cursor: pointer;\n font-size: 1rem;\n transition: all 0.2s;\n }\n\n .theme-toggle:hover {\n background: var(--bg-card-hover);\n border-color: var(--accent-primary);\n }\n\n .home-btn {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n padding: 0.5rem 1rem;\n background: var(--accent-gradient);\n color: white;\n border: none;\n border-radius: 8px;\n font-size: 0.875rem;\n font-weight: 500;\n cursor: pointer;\n text-decoration: none;\n transition: all 0.2s;\n }\n\n .home-btn:hover {\n transform: translateY(-1px);\n box-shadow: 0 4px 12px rgba(124, 58, 237, 0.3);\n }\n\n /* Main Container */\n .container {\n max-width: 1400px;\n margin: 0 auto;\n padding: 2rem;\n }\n\n /* Page Header */\n .page-header {\n text-align: center;\n margin-bottom: 2rem;\n }\n\n .page-header h1 {\n font-size: 2.5rem;\n font-weight: 800;\n margin-bottom: 0.5rem;\n background: var(--accent-gradient);\n -webkit-background-clip: text;\n -webkit-text-fill-color: transparent;\n }\n\n .page-header p {\n font-size: 1.1rem;\n color: var(--text-secondary);\n }\n\n /* Presets */\n .presets-grid {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));\n gap: 1rem;\n margin: 1.5rem 0;\n }\n\n .preset-card {\n padding: 1.25rem;\n background: var(--bg-card);\n border: 2px solid var(--border-color);\n border-radius: 12px;\n cursor: pointer;\n transition: all 0.2s;\n text-align: center;\n }\n\n .preset-card:hover {\n transform: translateY(-2px);\n border-color: var(--accent-primary);\n box-shadow: 0 4px 12px rgba(0, 166, 81, 0.25);\n }\n\n .preset-card.active {\n border-color: var(--accent-primary);\n background: rgba(0, 166, 81, 0.1);\n }\n\n .preset-card.recommended {\n border: 2px solid var(--accent-primary);\n }\n\n .preset-icon {\n font-size: 2rem;\n margin-bottom: 0.5rem;\n }\n\n .preset-name {\n font-weight: 600;\n margin-bottom: 0.25rem;\n color: var(--text-primary);\n }\n\n .preset-desc {\n font-size: 0.75rem;\n color: var(--text-secondary);\n }\n\n /* Config Sections */\n .config-section {\n background: var(--bg-card);\n border: 1px solid var(--border-color);\n border-radius: 16px;\n padding: 1.5rem;\n margin-bottom: 1.5rem;\n }\n\n .section-title {\n font-size: 1.125rem;\n font-weight: 700;\n margin-bottom: 1.5rem;\n display: flex;\n align-items: center;\n gap: 0.5rem;\n color: var(--text-primary);\n }\n\n /* Experts Grid */\n .experts-grid {\n display: grid;\n grid-template-columns: repeat(4, 1fr);\n gap: 1.5rem;\n }\n\n @media (max-width: 1200px) {\n .experts-grid {\n grid-template-columns: repeat(2, 1fr);\n }\n }\n\n @media (max-width: 600px) {\n .experts-grid {\n grid-template-columns: 1fr;\n }\n }\n\n .expert-card {\n background: var(--bg-secondary);\n border: 2px solid var(--border-color);\n border-radius: 12px;\n padding: 1.25rem;\n transition: all 0.2s;\n }\n\n .expert-card:hover {\n border-color: var(--accent-secondary);\n }\n\n .expert-card.expert-1 { border-top: 3px solid var(--accent-primary); }\n .expert-card.expert-2 { border-top: 3px solid var(--accent-primary); }\n .expert-card.expert-3 { border-top: 3px solid var(--accent-primary); }\n .expert-card.chairman { border-top: 3px solid var(--accent-secondary); }\n\n .expert-label {\n font-weight: 700;\n margin-bottom: 1rem;\n font-size: 0.95rem;\n display: flex;\n align-items: center;\n gap: 0.5rem;\n }\n\n .expert-1 .expert-label { color: var(--accent-primary); }\n .expert-2 .expert-label { color: var(--accent-primary); }\n .expert-3 .expert-label { color: var(--accent-primary); }\n .chairman .expert-label { color: var(--accent-secondary); }\n\n .form-group {\n margin-bottom: 1rem;\n }\n\n .form-group:last-child {\n margin-bottom: 0;\n }\n\n .form-group label {\n display: block;\n font-size: 0.75rem;\n font-weight: 600;\n color: var(--text-secondary);\n margin-bottom: 0.4rem;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n }\n\n .form-group select {\n width: 100%;\n padding: 0.75rem 1rem;\n background: var(--bg-card);\n border: 1px solid var(--border-color);\n border-radius: 8px;\n font-size: 0.875rem;\n color: var(--text-primary);\n cursor: pointer;\n transition: all 0.2s;\n appearance: none;\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%2394a3b8'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E\");\n background-repeat: no-repeat;\n background-position: right 0.75rem center;\n background-size: 1rem;\n padding-right: 2.5rem;\n }\n\n .form-group select:focus {\n outline: none;\n border-color: var(--accent-primary);\n box-shadow: 0 0 0 3px rgba(124, 58, 237, 0.15);\n }\n\n .form-group select option {\n background: var(--bg-card);\n color: var(--text-primary);\n padding: 0.5rem;\n }\n\n .strengths-box {\n font-size: 0.8rem;\n color: var(--text-secondary);\n padding: 0.5rem 0.75rem;\n background: rgba(0, 166, 81, 0.1);\n border-radius: 6px;\n margin-top: 0.75rem;\n }\n\n /* Estimate Box */\n .estimate-box {\n background: var(--accent-gradient);\n color: white;\n padding: 1.5rem;\n border-radius: 12px;\n margin-bottom: 1.5rem;\n }\n\n .estimate-title {\n font-size: 1.1rem;\n font-weight: 700;\n margin-bottom: 1rem;\n }\n\n .estimate-grid {\n display: grid;\n grid-template-columns: repeat(3, 1fr);\n gap: 1rem;\n }\n\n @media (max-width: 600px) {\n .estimate-grid {\n grid-template-columns: 1fr;\n }\n }\n\n .estimate-item {\n background: rgba(255,255,255,0.15);\n padding: 1rem;\n border-radius: 8px;\n text-align: center;\n }\n\n .estimate-label {\n font-size: 0.85rem;\n opacity: 0.9;\n }\n\n .estimate-value {\n font-size: 1.5rem;\n font-weight: 700;\n margin-top: 0.25rem;\n }\n\n .warning-box {\n display: none;\n margin-top: 1rem;\n padding: 0.75rem;\n background: rgba(245, 158, 11, 0.2);\n border-radius: 8px;\n font-size: 0.875rem;\n }\n\n /* Query Section */\n .query-section textarea {\n width: 100%;\n min-height: 150px;\n padding: 1rem;\n background: var(--input-bg);\n border: 2px solid var(--input-border);\n border-radius: 12px;\n font-size: 0.95rem;\n color: var(--input-text);\n resize: vertical;\n transition: all 0.2s;\n line-height: 1.6;\n }\n\n .query-section textarea::placeholder {\n color: var(--input-placeholder);\n }\n\n .query-section textarea:focus {\n outline: none;\n border-color: var(--accent-primary);\n box-shadow: 0 0 0 3px rgba(124, 58, 237, 0.15);\n }\n\n /* Options Row */\n .options-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n margin-top: 1rem;\n flex-wrap: wrap;\n gap: 1rem;\n }\n\n .checkbox-label {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n cursor: pointer;\n font-size: 0.9rem;\n color: var(--text-secondary);\n }\n\n .checkbox-label input[type=\"checkbox\"] {\n width: 1.25rem;\n height: 1.25rem;\n cursor: pointer;\n accent-color: var(--accent-primary);\n }\n\n /* Submit Button */\n .btn-submit {\n padding: 1rem 2rem;\n background: var(--accent-gradient);\n color: white;\n border: none;\n border-radius: 10px;\n font-size: 1rem;\n font-weight: 600;\n cursor: pointer;\n transition: all 0.2s;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 0.5rem;\n }\n\n .btn-submit:hover:not(:disabled) {\n transform: translateY(-1px);\n box-shadow: 0 6px 20px rgba(124, 58, 237, 0.35);\n }\n\n .btn-submit:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n }\n\n /* Loading */\n .loading-box {\n display: none;\n padding: 1.5rem;\n background: var(--warning-bg);\n border-left: 4px solid var(--warning);\n border-radius: 8px;\n margin-bottom: 1.5rem;\n }\n\n .loading-content {\n display: flex;\n align-items: center;\n gap: 1rem;\n }\n\n .spinner {\n border: 3px solid rgba(245, 158, 11, 0.3);\n border-top-color: var(--warning);\n border-radius: 50%;\n width: 24px;\n height: 24px;\n animation: spin 0.8s linear infinite;\n }\n\n @keyframes spin {\n to { transform: rotate(360deg); }\n }\n\n /* Results Section */\n .results-section {\n display: none;\n }\n\n .final-response-box {\n background: linear-gradient(135deg, rgba(16, 185, 129, 0.15), rgba(5, 150, 105, 0.1));\n border: 1px solid rgba(16, 185, 129, 0.3);\n border-left: 4px solid var(--success);\n border-radius: 16px;\n padding: 2rem;\n margin-bottom: 1.5rem;\n }\n\n .result-header {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n margin-bottom: 1rem;\n font-size: 1.25rem;\n font-weight: 700;\n color: var(--success);\n }\n\n .result-text {\n font-size: 1rem;\n line-height: 1.8;\n color: var(--text-primary);\n white-space: pre-wrap;\n }\n\n .result-meta {\n margin-top: 1.5rem;\n padding-top: 1rem;\n border-top: 1px solid rgba(16, 185, 129, 0.2);\n display: flex;\n gap: 2rem;\n font-size: 0.875rem;\n color: var(--text-secondary);\n }\n\n /* Opinions Section */\n .opinions-box {\n background: var(--bg-card);\n border: 1px solid var(--border-color);\n border-radius: 16px;\n padding: 1.5rem;\n }\n\n .opinion-tabs {\n display: flex;\n gap: 0.5rem;\n margin-bottom: 1.5rem;\n border-bottom: 2px solid var(--border-color);\n padding-bottom: 0.5rem;\n overflow-x: auto;\n }\n\n .opinion-tab {\n padding: 0.75rem 1.25rem;\n background: transparent;\n border: none;\n border-bottom: 3px solid transparent;\n cursor: pointer;\n font-weight: 500;\n color: var(--text-secondary);\n transition: all 0.2s;\n white-space: nowrap;\n }\n\n .opinion-tab:hover {\n color: var(--text-primary);\n }\n\n .opinion-tab.active {\n color: var(--accent-primary);\n border-bottom-color: var(--accent-primary);\n }\n\n .opinion-content {\n padding: 1.5rem;\n background: var(--bg-secondary);\n border-radius: 12px;\n line-height: 1.8;\n white-space: pre-wrap;\n color: var(--text-primary);\n max-height: 400px;\n overflow-y: auto;\n }\n\n /* Error Toast */\n .error-toast {\n position: fixed;\n bottom: 2rem;\n right: 2rem;\n padding: 1rem 1.5rem;\n background: var(--error);\n color: white;\n border-radius: 10px;\n font-size: 0.875rem;\n display: none;\n z-index: 1000;\n animation: slideIn 0.3s ease;\n max-width: 400px;\n }\n\n .error-toast.visible {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n }\n\n @keyframes slideIn {\n from {\n transform: translateY(20px);\n opacity: 0;\n }\n to {\n transform: translateY(0);\n opacity: 1;\n }\n }\n\n /* Scrollbar */\n ::-webkit-scrollbar {\n width: 8px;\n height: 8px;\n }\n\n ::-webkit-scrollbar-track {\n background: transparent;\n }\n\n ::-webkit-scrollbar-thumb {\n background: var(--border-color);\n border-radius: 4px;\n }\n\n ::-webkit-scrollbar-thumb:hover {\n background: var(--text-muted);\n }\n </style>\n <link rel=\"stylesheet\" href=\"/apps/shared/iafactory-unified.css\">\n <style>\n\n :root {\n --bg: #020617;\n --bg-alt: #020617;\n --card: #020617;\n --bg-card: #020617;\n --bg-secondary: #0f172a;\n --bg-dark: #020617;\n --border: rgba(255, 255, 255, 0.12);\n --border-primary: rgba(255, 255, 255, 0.12);\n --primary: #00a651;\n --primary-dark: #008c45;\n --accent: #00a651;\n --accent-primary: #00a651;\n --text: #f8fafc;\n --text-primary: #f8fafc;\n --text-secondary: rgba(248, 250, 252, 0.75);\n --muted: rgba(248, 250, 252, 0.75);\n --shadow: 0 20px 60px rgba(0, 0, 0, 0.55);\n }\n\n [data-theme=\"light\"] {\n --bg: #f7f5f0;\n --bg-alt: #f7f5f0;\n --card: #f7f5f0;\n --bg-card: #ffffff;\n --bg-secondary: #f7f5f0;\n --bg-dark: #f7f5f0;\n --border: rgba(0, 0, 0, 0.08);\n --border-primary: rgba(0, 0, 0, 0.08);\n --primary: #00a651;\n --primary-dark: #008c45;\n --accent: #00a651;\n --accent-primary: #00a651;\n --text: #0f172a;\n --text-primary: #0f172a;\n --text-secondary: rgba(15, 23, 42, 0.7);\n --muted: rgba(15, 23, 42, 0.7);\n --shadow: 0 20px 60px rgba(15, 23, 42, 0.25);\n }\n </style>\n</head>\n<body>\n <!-- Header -->\n <header class=\"main-header\">\n <div class=\"logo\">\n <div class=\"logo-icon\">\n <i class=\"fas fa-users\"></i>\n </div>\n <h1>Council IA</h1>\n </div>\n <div class=\"header-actions\">\n <button class=\"theme-toggle\" onclick=\"toggleTheme()\" title=\"Changer le thème\">\n <i class=\"fas fa-moon\" id=\"theme-icon\"></i>\n </button>\n <a href=\"/\" class=\"home-btn\">\n <i class=\"fas fa-home\"></i>\n Accueil\n </a>\n </div>\n \n <!-- IAF Unified Controls -->\n <div class=\"iaf-header-actions\" style=\"display:flex;align-items:center;gap:0.5rem;margin-left:auto;\">\n <div class=\"iaf-lang-dropdown\" style=\"position:relative;\">\n <button class=\"iaf-lang-btn\" onclick=\"IAFactory.toggleLangMenu()\" style=\"display:flex;align-items:center;gap:0.5rem;padding:0.5rem 0.75rem;border-radius:8px;border:1px solid rgba(255,255,255,0.1);background:transparent;color:inherit;cursor:pointer;font-size:0.85rem;\">\n 🌐 <span id=\"iaf-current-lang\">FR</span>\n </button>\n <div class=\"iaf-lang-menu\" id=\"iaf-lang-menu\" style=\"position:absolute;top:100%;right:0;margin-top:4px;background:var(--bg-card,#1a1a2e);border:1px solid rgba(255,255,255,0.1);border-radius:12px;box-shadow:0 15px 30px rgba(0,0,0,0.3);display:none;min-width:150px;z-index:1001;overflow:hidden;\">\n <button class=\"iaf-lang-option\" onclick=\"IAFactory.setLanguage('fr')\" style=\"display:flex;align-items:center;gap:0.5rem;width:100%;padding:0.75rem 1rem;border:none;background:transparent;color:inherit;cursor:pointer;text-align:left;\">🇫🇷 Français</button>\n <button class=\"iaf-lang-option\" onclick=\"IAFactory.setLanguage('en')\" style=\"display:flex;align-items:center;gap:0.5rem;width:100%;padding:0.75rem 1rem;border:none;background:transparent;color:inherit;cursor:pointer;text-align:left;\">🇬🇧 English</button>\n <button class=\"iaf-lang-option\" onclick=\"IAFactory.setLanguage('ar')\" style=\"display:flex;align-items:center;gap:0.5rem;width:100%;padding:0.75rem 1rem;border:none;background:transparent;color:inherit;cursor:pointer;text-align:left;\">🇩🇿 العربية</button>\n </div>\n </div>\n <button class=\"iaf-theme-btn\" onclick=\"IAFactory.toggleTheme()\" id=\"iaf-theme-btn\" style=\"width:40px;height:40px;border-radius:10px;border:1px solid rgba(255,255,255,0.1);background:transparent;color:inherit;cursor:pointer;font-size:1.1rem;\">☀️</button>\n </div>\n\n </header>\n\n <div class=\"container\">\n <!-- Page Header -->\n <div class=\"page-header\">\n <h1>🎯 Build Your Council</h1>\n <p>Composez votre équipe d'experts IA sur mesure avec une infinité de combinaisons</p>\n </div>\n\n <!-- Presets -->\n <div class=\"config-section\">\n <h2 class=\"section-title\">✨ Configurations recommandées (Groq ⚡ + OpenRouter 🌐)</h2>\n <div class=\"presets-grid\" id=\"presetsGrid\">\n <div class=\"preset-card recommended\" onclick=\"applyPreset('fast')\">\n <div class=\"preset-icon\">⚡</div>\n <div class=\"preset-name\">Groq Diversifié ⭐</div>\n <div class=\"preset-desc\">Llama 70B + Qwen + Kimi</div>\n </div>\n <div class=\"preset-card recommended\" onclick=\"applyPreset('deepseek')\">\n <div class=\"preset-icon\">🇨🇳</div>\n <div class=\"preset-name\">DeepSeek Power ⭐</div>\n <div class=\"preset-desc\">Chat + R1 Reasoner</div>\n </div>\n <div class=\"preset-card\" onclick=\"applyPreset('diversified')\">\n <div class=\"preset-icon\">🎯</div>\n <div class=\"preset-name\">Mix Groq+DeepSeek</div>\n <div class=\"preset-desc\">Llama + Kimi + DeepSeek</div>\n </div>\n <div class=\"preset-card\" onclick=\"applyPreset('llama4')\">\n <div class=\"preset-icon\">🦙</div>\n <div class=\"preset-name\">Llama 4 Meta</div>\n <div class=\"preset-desc\">Maverick + Scout + 70B</div>\n </div>\n <div class=\"preset-card\" onclick=\"applyPreset('openrouter')\">\n <div class=\"preset-icon\">🌐</div>\n <div class=\"preset-name\">OpenRouter FREE</div>\n <div class=\"preset-desc\">Gemini + Llama + Mistral</div>\n </div>\n <div class=\"preset-card\" onclick=\"applyPreset('hybrid')\">\n <div class=\"preset-icon\">🔀</div>\n <div class=\"preset-name\">Hybride</div>\n <div class=\"preset-desc\">Groq + Gemini + Qwen</div>\n </div>\n <div class=\"preset-card\" onclick=\"applyPreset('local')\">\n <div class=\"preset-icon\">🏠</div>\n <div class=\"preset-name\">100% Local</div>\n <div class=\"preset-desc\">Ollama (lent sur VPS)</div>\n </div>\n </div>\n </div>\n\n <!-- Configuration -->\n <div class=\"config-section\">\n <h2 class=\"section-title\">⚙️ Configuration du Council</h2>\n \n <div class=\"experts-grid\">\n <!-- Expert 1 -->\n <div class=\"expert-card expert-1\">\n <div class=\"expert-label\">\n <i class=\"fas fa-user-tie\"></i> Expert 1\n </div>\n <div class=\"form-group\">\n <label>Provider</label>\n <select id=\"provider1\" onchange=\"onProviderChange(1)\">\n <option value=\"deepseek\">🇨🇳 DeepSeek (Économique)</option>\n <option value=\"groq\" selected>⚡ Groq (Ultra-rapide)</option>\n <option value=\"openrouter\">🌐 OpenRouter</option>\n <option value=\"ollama\">🏠 Ollama (Local)</option>\n <option value=\"openai\">🤖 OpenAI (ChatGPT)</option>\n <option value=\"anthropic\">🎭 Anthropic (Claude)</option>\n <option value=\"perplexity\">🔍 Perplexity</option>\n <option value=\"grok\">🚀 Grok (xAI)</option>\n <option value=\"mistral\">🇫🇷 Mistral</option>\n </select>\n </div>\n <div class=\"form-group\">\n <label>Modèle</label>\n <select id=\"model1\"></select>\n </div>\n <div class=\"strengths-box\" id=\"strengths1\">💪 Sélectionnez un modèle</div>\n </div>\n\n <!-- Expert 2 -->\n <div class=\"expert-card expert-2\">\n <div class=\"expert-label\">\n <i class=\"fas fa-user-tie\"></i> Expert 2\n </div>\n <div class=\"form-group\">\n <label>Provider</label>\n <select id=\"provider2\" onchange=\"onProviderChange(2)\">\n <option value=\"deepseek\">🇨🇳 DeepSeek (Économique)</option>\n <option value=\"groq\" selected>⚡ Groq (Ultra-rapide)</option>\n <option value=\"openrouter\">🌐 OpenRouter</option>\n <option value=\"ollama\">🏠 Ollama (Local)</option>\n <option value=\"openai\">🤖 OpenAI (ChatGPT)</option>\n <option value=\"anthropic\">🎭 Anthropic (Claude)</option>\n <option value=\"perplexity\">🔍 Perplexity</option>\n <option value=\"grok\">🚀 Grok (xAI)</option>\n <option value=\"mistral\">🇫🇷 Mistral</option>\n </select>\n </div>\n <div class=\"form-group\">\n <label>Modèle</label>\n <select id=\"model2\"></select>\n </div>\n <div class=\"strengths-box\" id=\"strengths2\">💪 Sélectionnez un modèle</div>\n </div>\n\n <!-- Expert 3 -->\n <div class=\"expert-card expert-3\">\n <div class=\"expert-label\">\n <i class=\"fas fa-user-tie\"></i> Expert 3\n </div>\n <div class=\"form-group\">\n <label>Provider</label>\n <select id=\"provider3\" onchange=\"onProviderChange(3)\">\n <option value=\"deepseek\">🇨🇳 DeepSeek (Économique)</option>\n <option value=\"groq\" selected>⚡ Groq (Ultra-rapide)</option>\n <option value=\"openrouter\">🌐 OpenRouter</option>\n <option value=\"ollama\">🏠 Ollama (Local)</option>\n <option value=\"openai\">🤖 OpenAI (ChatGPT)</option>\n <option value=\"anthropic\">🎭 Anthropic (Claude)</option>\n <option value=\"perplexity\">🔍 Perplexity</option>\n <option value=\"grok\">🚀 Grok (xAI)</option>\n <option value=\"mistral\">🇫🇷 Mistral</option>\n </select>\n </div>\n <div class=\"form-group\">\n <label>Modèle</label>\n <select id=\"model3\"></select>\n </div>\n <div class=\"strengths-box\" id=\"strengths3\">💪 Sélectionnez un modèle</div>\n </div>\n\n <!-- Chairman -->\n <div class=\"expert-card chairman\">\n <div class=\"expert-label\">\n <i class=\"fas fa-crown\"></i> Chairman (Synthèse)\n </div>\n <div class=\"form-group\">\n <label>Provider</label>\n <select id=\"providerChairman\" onchange=\"onProviderChange('Chairman')\">\n <option value=\"deepseek\">🇨🇳 DeepSeek (Économique)</option>\n <option value=\"groq\" selected>⚡ Groq (Ultra-rapide)</option>\n <option value=\"openrouter\">🌐 OpenRouter</option>\n <option value=\"ollama\">🏠 Ollama (Local)</option>\n <option value=\"openai\">🤖 OpenAI (ChatGPT)</option>\n <option value=\"anthropic\">🎭 Anthropic (Claude)</option>\n <option value=\"perplexity\">🔍 Perplexity</option>\n <option value=\"grok\">🚀 Grok (xAI)</option>\n <option value=\"mistral\">🇫🇷 Mistral</option>\n </select>\n </div>\n <div class=\"form-group\">\n <label>Modèle</label>\n <select id=\"modelChairman\"></select>\n </div>\n <div class=\"strengths-box\" id=\"strengthsChairman\">💪 Sélectionnez un modèle</div>\n </div>\n </div>\n </div>\n\n <!-- Estimation -->\n <div class=\"estimate-box\">\n <div class=\"estimate-title\">📊 Estimation</div>\n <div class=\"estimate-grid\">\n <div class=\"estimate-item\">\n <div class=\"estimate-label\">💰 Coût estimé</div>\n <div class=\"estimate-value\" id=\"costEstimate\">~50 DZD</div>\n </div>\n <div class=\"estimate-item\">\n <div class=\"estimate-label\">⏱️ Temps estimé</div>\n <div class=\"estimate-value\" id=\"timeEstimate\">~15s</div>\n </div>\n <div class=\"estimate-item\">\n <div class=\"estimate-label\">🎯 Combinaisons</div>\n <div class=\"estimate-value\">∞</div>\n </div>\n </div>\n <div class=\"warning-box\" id=\"warningBox\">\n ⚠️ Configuration premium : coût élevé. Utilisez Groq pour réduire les coûts.\n </div>\n </div>\n\n <!-- Query Section -->\n <div class=\"config-section query-section\">\n <h2 class=\"section-title\">💬 Votre question</h2>\n <textarea \n id=\"queryInput\" \n placeholder=\"Posez votre question au Council personnalisé...\n\nExemples:\n• Quelles sont les meilleures pratiques pour sécuriser une API REST ?\n• Comment optimiser PostgreSQL pour des millions de lignes ?\n• Expliquez la différence entre authentification et autorisation\n• Compare les architectures microservices vs monolithique\"\n ></textarea>\n \n <div class=\"options-row\">\n <label class=\"checkbox-label\">\n <input type=\"checkbox\" id=\"enableReview\">\n <span>Activer la revue croisée (+50% temps, meilleure qualité)</span>\n </label>\n <button class=\"btn-submit\" id=\"submitBtn\" onclick=\"executeCouncil()\">\n <i class=\"fas fa-bolt\"></i>\n Lancer le Council\n </button>\n </div>\n </div>\n\n <!-- Loading -->\n <div class=\"loading-box\" id=\"loadingBox\">\n <div class=\"loading-content\">\n <div class=\"spinner\"></div>\n <div>\n <div style=\"font-weight: 600; margin-bottom: 0.25rem;\">Council en délibération...</div>\n <div style=\"font-size: 0.875rem; color: var(--text-secondary);\" id=\"loadingStatus\">Les experts analysent votre question</div>\n </div>\n </div>\n </div>\n\n <!-- Results -->\n <div class=\"results-section\" id=\"resultsSection\">\n <div class=\"final-response-box\">\n <div class=\"result-header\">\n <i class=\"fas fa-crown\"></i>\n <span>Réponse finale du Chairman</span>\n <span id=\"chairmanName\" style=\"font-size: 0.9rem; font-weight: normal; color: var(--text-secondary);\"></span>\n </div>\n <div class=\"result-text\" id=\"finalResponse\"></div>\n <div class=\"result-meta\">\n <span id=\"execTime\"><i class=\"fas fa-clock\"></i> -</span>\n <span id=\"actualCost\"><i class=\"fas fa-coins\"></i> -</span>\n <span id=\"expertsCount\"><i class=\"fas fa-users\"></i> -</span>\n </div>\n </div>\n\n <div class=\"opinions-box\">\n <h3 class=\"section-title\">🎯 Opinions individuelles des experts</h3>\n <div class=\"opinion-tabs\" id=\"opinionTabs\"></div>\n <div class=\"opinion-content\" id=\"opinionContent\">Sélectionnez un expert pour voir son opinion</div>\n </div>\n </div>\n </div>\n\n <!-- Error Toast -->\n <div class=\"error-toast\" id=\"errorToast\">\n <i class=\"fas fa-exclamation-circle\"></i>\n <span id=\"errorMessage\"></span>\n </div>\n\n <script>\n // ============================================\n // Provider and Model Configuration\n // ============================================\n // API Keys pour appels directs\n // ============================================\n const OPENROUTER_API_KEY = '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';\n const GROQ_API_KEY = 'YOUR_GROQ_API_KEY';\n const DEEPSEEK_API_KEY = 'sk-e2d7d214600946479856ffafbe1ce392';\n const OPENAI_API_KEY = 'YOUR_OPENAI_API_KEY';\n const ANTHROPIC_API_KEY = 'YOUR_ANTHROPIC_API_KEY';\n const MISTRAL_API_KEY = 'U4TD40GfA96d4txjFQzQSps2PXaxKYHC';\n const GOOGLE_API_KEY = 'AIzaSyB21Sv2aZEJ33TJ02dfLMx-PklP4ZVcG40';\n const COHERE_API_KEY = 'bAVVqL7U4wv2gmd4gbGBQwpVXDEGZqFm42czSg3a';\n const TOGETHER_API_KEY = '99ac6265846d3cb6a8f83e038605d01a4d1c6b8b80e4a64d3486460a89b85df3';\n const PERPLEXITY_API_KEY = 'YOUR_PERPLEXITY_API_KEY';\n const GROK_API_KEY = 'YOUR_XAI_API_KEY';\n \n // APIs Média (Vidéo/Avatar)\n const DID_API_KEY = 'YmVuc2FsZW1ib3VhbGVtQGdtYWlsLmNvbQ:XDqHY1ZQh9pWKHviJqMed';\n\n const PROVIDERS = {\n deepseek: {\n name: 'DeepSeek 🇨🇳',\n models: [\n { id: 'deepseek.deepseek-chat', name: 'DeepSeek Chat V3', strengths: ['Économique', 'Raisonnement', 'Code'], cost: 1 },\n { id: 'deepseek.deepseek-reasoner', name: 'DeepSeek R1 🧠', strengths: ['Raisonnement avancé', 'Math', 'Logique'], cost: 3 }\n ]\n },\n groq: {\n name: 'Groq ⚡',\n models: [\n { id: 'groq.llama-3.3-70b-versatile', name: 'Llama 3.3 70B', strengths: ['Puissant', 'Versatile', '131K ctx'], cost: 0 },\n { id: 'groq.llama-3.1-8b-instant', name: 'Llama 3.1 8B', strengths: ['Ultra-rapide', 'Gratuit'], cost: 0 },\n { id: 'groq.moonshotai/kimi-k2-instruct', name: 'Kimi K2 🌙', strengths: ['Moonshot AI', 'Raisonnement'], cost: 0 },\n { id: 'groq.qwen/qwen3-32b', name: 'Qwen3 32B', strengths: ['Alibaba', 'Multilingue', 'Code'], cost: 0 },\n { id: 'groq.meta-llama/llama-4-maverick-17b-128e-instruct', name: 'Llama 4 Maverick 🦙', strengths: ['Meta', 'Nouveau', '131K ctx'], cost: 0 },\n { id: 'groq.meta-llama/llama-4-scout-17b-16e-instruct', name: 'Llama 4 Scout 🦙', strengths: ['Meta', 'Exploration'], cost: 0 },\n { id: 'groq.openai/gpt-oss-120b', name: 'GPT-OSS 120B', strengths: ['OpenAI OSS', 'Très puissant'], cost: 0 },\n { id: 'groq.openai/gpt-oss-20b', name: 'GPT-OSS 20B', strengths: ['OpenAI OSS', 'Rapide'], cost: 0 },\n { id: 'groq.allam-2-7b', name: 'Allam 2 7B 🇸🇦', strengths: ['Arabe natif', 'SDAIA'], cost: 0 }\n ]\n },\n openrouter: {\n name: 'OpenRouter 🌐',\n models: [\n { id: 'openrouter.google/gemini-2.0-flash-exp:free', name: 'Gemini 2.0 Flash (Free)', strengths: ['Gratuit', 'Google', 'Rapide'], cost: 0 },\n { id: 'openrouter.meta-llama/llama-3.2-3b-instruct:free', name: 'Llama 3.2 3B (Free)', strengths: ['Gratuit', 'Meta', 'Compact'], cost: 0 },\n { id: 'openrouter.mistralai/mistral-7b-instruct:free', name: 'Mistral 7B (Free)', strengths: ['Gratuit', 'FR natif'], cost: 0 },\n { id: 'openrouter.deepseek/deepseek-chat', name: 'DeepSeek Chat 🇨🇳', strengths: ['Économique', 'Code', 'Raisonnement'], cost: 2 },\n { id: 'openrouter.deepseek/deepseek-r1', name: 'DeepSeek R1 🧠', strengths: ['Raisonnement avancé', 'Math'], cost: 5 },\n { id: 'openrouter.qwen/qwen-2.5-72b-instruct', name: 'Qwen 2.5 72B', strengths: ['Alibaba', 'Puissant'], cost: 10 },\n { id: 'openrouter.anthropic/claude-3.5-sonnet', name: 'Claude 3.5 Sonnet', strengths: ['Anthropic', 'Écriture'], cost: 15 },\n { id: 'openrouter.openai/gpt-4o-mini', name: 'GPT-4o Mini', strengths: ['OpenAI', 'Rapide'], cost: 5 },\n { id: 'openrouter.openai/gpt-4o', name: 'GPT-4o', strengths: ['OpenAI', 'Meilleur'], cost: 25 }\n ]\n },\n ollama: {\n name: 'Ollama 🏠 (lent)',\n models: [\n { id: 'ollama.llama3.2:3b', name: 'Llama 3.2 3B', strengths: ['Local', 'Privé', 'Rapide'], cost: 0 },\n { id: 'ollama.mistral:7b', name: 'Mistral 7B', strengths: ['Local', 'FR natif', 'Équilibré'], cost: 0 },\n { id: 'ollama.qwen2.5:3b', name: 'Qwen 2.5 3B', strengths: ['Local', 'Multilingue', 'Compact'], cost: 0 }\n ]\n },\n // ============ PROVIDERS PREMIUM (nécessitent clé API) ============\n openai: {\n name: 'OpenAI 🤖 (ChatGPT)',\n models: [\n { id: 'openai.gpt-4o', name: 'GPT-4o', strengths: ['Meilleur', 'Multimodal', '128K ctx'], cost: 25 },\n { id: 'openai.gpt-4o-mini', name: 'GPT-4o Mini', strengths: ['Rapide', 'Économique'], cost: 5 },\n { id: 'openai.gpt-4-turbo', name: 'GPT-4 Turbo', strengths: ['Puissant', '128K ctx'], cost: 30 },\n { id: 'openai.gpt-4', name: 'GPT-4', strengths: ['Original', 'Stable'], cost: 30 },\n { id: 'openai.gpt-3.5-turbo', name: 'GPT-3.5 Turbo', strengths: ['Économique', 'Rapide'], cost: 2 },\n { id: 'openai.o1-preview', name: 'O1 Preview 🧠', strengths: ['Raisonnement', 'Math', 'Code'], cost: 60 },\n { id: 'openai.o1-mini', name: 'O1 Mini 🧠', strengths: ['Raisonnement', 'Économique'], cost: 12 }\n ]\n },\n anthropic: {\n name: 'Anthropic 🎭 (Claude)',\n models: [\n { id: 'anthropic.claude-3-5-sonnet-20241022', name: 'Claude 3.5 Sonnet', strengths: ['Meilleur', 'Code', 'Écriture'], cost: 15 },\n { id: 'anthropic.claude-3-5-haiku-20241022', name: 'Claude 3.5 Haiku', strengths: ['Rapide', 'Économique'], cost: 4 },\n { id: 'anthropic.claude-3-opus-20240229', name: 'Claude 3 Opus', strengths: ['Très puissant', 'Analyse'], cost: 75 },\n { id: 'anthropic.claude-3-sonnet-20240229', name: 'Claude 3 Sonnet', strengths: ['Équilibré', 'Versatile'], cost: 15 },\n { id: 'anthropic.claude-3-haiku-20240307', name: 'Claude 3 Haiku', strengths: ['Ultra-rapide', 'Compact'], cost: 1 }\n ]\n },\n perplexity: {\n name: 'Perplexity 🔍',\n models: [\n { id: 'perplexity.llama-3.1-sonar-large-128k-online', name: 'Sonar Large Online', strengths: ['Recherche web', 'Temps réel', '128K'], cost: 5 },\n { id: 'perplexity.llama-3.1-sonar-small-128k-online', name: 'Sonar Small Online', strengths: ['Recherche web', 'Économique'], cost: 2 },\n { id: 'perplexity.llama-3.1-sonar-huge-128k-online', name: 'Sonar Huge Online', strengths: ['Le plus puissant', 'Recherche'], cost: 20 },\n { id: 'perplexity.llama-3.1-sonar-large-128k-chat', name: 'Sonar Large Chat', strengths: ['Sans recherche', 'Rapide'], cost: 3 },\n { id: 'perplexity.llama-3.1-sonar-small-128k-chat', name: 'Sonar Small Chat', strengths: ['Sans recherche', 'Économique'], cost: 1 }\n ]\n },\n grok: {\n name: 'Grok 🚀 (xAI)',\n models: [\n { id: 'grok.grok-beta', name: 'Grok Beta', strengths: ['Nouveau', 'Humour', 'Temps réel'], cost: 10 },\n { id: 'grok.grok-2-1212', name: 'Grok 2', strengths: ['Dernière version', 'Puissant'], cost: 15 },\n { id: 'grok.grok-2-vision-1212', name: 'Grok 2 Vision', strengths: ['Multimodal', 'Images'], cost: 20 }\n ]\n },\n mistral: {\n name: 'Mistral 🇫🇷',\n models: [\n { id: 'mistral.mistral-large-latest', name: 'Mistral Large', strengths: ['Le meilleur', 'FR natif', '128K'], cost: 12 },\n { id: 'mistral.mistral-medium-latest', name: 'Mistral Medium', strengths: ['Équilibré', 'Polyvalent'], cost: 8 },\n { id: 'mistral.mistral-small-latest', name: 'Mistral Small', strengths: ['Rapide', 'Économique'], cost: 3 },\n { id: 'mistral.open-mistral-nemo', name: 'Mistral Nemo', strengths: ['128K ctx', 'Multilingue'], cost: 2 },\n { id: 'mistral.codestral-latest', name: 'Codestral', strengths: ['Code', 'Programmation'], cost: 3 },\n { id: 'mistral.ministral-8b-latest', name: 'Ministral 8B', strengths: ['Compact', 'Edge'], cost: 1 },\n { id: 'mistral.ministral-3b-latest', name: 'Ministral 3B', strengths: ['Ultra-compact', 'Rapide'], cost: 0.5 },\n { id: 'mistral.pixtral-large-latest', name: 'Pixtral Large', strengths: ['Vision', 'Images'], cost: 15 }\n ]\n }\n };\n\n // NOTE: ⚡ = Groq ultra-rapide, 🌐 = OpenRouter (gratuit/payant), 🏠 = Local (lent sur VPS)\n\n const PRESETS = {\n openrouter: {\n name: 'OpenRouter Free',\n experts: [\n { provider: 'openrouter', model: 'openrouter.google/gemini-2.0-flash-exp:free' },\n { provider: 'openrouter', model: 'openrouter.meta-llama/llama-3.2-3b-instruct:free' },\n { provider: 'openrouter', model: 'openrouter.mistralai/mistral-7b-instruct:free' }\n ],\n chairman: { provider: 'openrouter', model: 'openrouter.google/gemini-2.0-flash-exp:free' }\n },\n fast: {\n name: 'Groq Ultra Rapide',\n experts: [\n { provider: 'groq', model: 'groq.llama-3.3-70b-versatile' },\n { provider: 'groq', model: 'groq.qwen/qwen3-32b' },\n { provider: 'groq', model: 'groq.moonshotai/kimi-k2-instruct' }\n ],\n chairman: { provider: 'groq', model: 'groq.llama-3.3-70b-versatile' }\n },\n diversified: {\n name: 'Diversifié',\n experts: [\n { provider: 'groq', model: 'groq.llama-3.3-70b-versatile' },\n { provider: 'groq', model: 'groq.moonshotai/kimi-k2-instruct' },\n { provider: 'deepseek', model: 'deepseek.deepseek-chat' }\n ],\n chairman: { provider: 'deepseek', model: 'deepseek.deepseek-reasoner' }\n },\n deepseek: {\n name: 'DeepSeek Power 🇨🇳',\n experts: [\n { provider: 'deepseek', model: 'deepseek.deepseek-chat' },\n { provider: 'deepseek', model: 'deepseek.deepseek-reasoner' },\n { provider: 'groq', model: 'groq.llama-3.3-70b-versatile' }\n ],\n chairman: { provider: 'deepseek', model: 'deepseek.deepseek-reasoner' }\n },\n llama4: {\n name: 'Llama 4 🦙',\n experts: [\n { provider: 'groq', model: 'groq.meta-llama/llama-4-maverick-17b-128e-instruct' },\n { provider: 'groq', model: 'groq.meta-llama/llama-4-scout-17b-16e-instruct' },\n { provider: 'groq', model: 'groq.llama-3.3-70b-versatile' }\n ],\n chairman: { provider: 'groq', model: 'groq.meta-llama/llama-4-maverick-17b-128e-instruct' }\n },\n hybrid: {\n name: 'Hybride',\n experts: [\n { provider: 'groq', model: 'groq.llama-3.3-70b-versatile' },\n { provider: 'openrouter', model: 'openrouter.google/gemini-2.0-flash-exp:free' },\n { provider: 'groq', model: 'groq.qwen/qwen3-32b' }\n ],\n chairman: { provider: 'groq', model: 'groq.moonshotai/kimi-k2-instruct' }\n },\n local: {\n name: '100% Local',\n experts: [\n { provider: 'ollama', model: 'ollama.llama3.2:3b' },\n { provider: 'ollama', model: 'ollama.mistral:7b' },\n { provider: 'ollama', model: 'ollama.qwen2.5:3b' }\n ],\n chairman: { provider: 'ollama', model: 'ollama.mistral:7b' }\n }\n };\n\n let currentOpinions = {};\n\n // ============================================\n // Initialization\n // ============================================\n document.addEventListener('DOMContentLoaded', () => {\n // Load theme\n const savedTheme = localStorage.getItem('council-theme') || 'dark';\n document.documentElement.setAttribute('data-theme', savedTheme);\n updateThemeIcon(savedTheme);\n\n // Initialize all expert selects\n [1, 2, 3, 'Chairman'].forEach(num => {\n onProviderChange(num);\n });\n\n updateEstimate();\n });\n\n // ============================================\n // Theme Toggle\n // ============================================\n\n function updateThemeIcon(theme) {\n const icon = document.getElementById('theme-icon');\n icon.className = theme === 'dark' ? 'fas fa-moon' : 'fas fa-sun';\n }\n\n // ============================================\n // Provider/Model Management\n // ============================================\n function onProviderChange(expertNum) {\n const providerSelect = document.getElementById(`provider${expertNum}`);\n const modelSelect = document.getElementById(`model${expertNum}`);\n const strengthsBox = document.getElementById(`strengths${expertNum}`);\n \n const provider = providerSelect.value;\n const models = PROVIDERS[provider].models;\n\n // Populate models\n modelSelect.innerHTML = models.map(m => \n `<option value=\"${m.id}\">${m.name}</option>`\n ).join('');\n\n // Update strengths\n updateStrengths(expertNum);\n \n // Add change listener to model\n modelSelect.onchange = () => updateStrengths(expertNum);\n\n updateEstimate();\n }\n\n function updateStrengths(expertNum) {\n const providerSelect = document.getElementById(`provider${expertNum}`);\n const modelSelect = document.getElementById(`model${expertNum}`);\n const strengthsBox = document.getElementById(`strengths${expertNum}`);\n\n const provider = providerSelect.value;\n const modelId = modelSelect.value;\n const model = PROVIDERS[provider].models.find(m => m.id === modelId);\n\n if (model) {\n strengthsBox.textContent = `💪 ${model.strengths.join(', ')}`;\n }\n\n updateEstimate();\n }\n\n // ============================================\n // Presets\n // ============================================\n function applyPreset(presetName) {\n const preset = PRESETS[presetName];\n \n // Update presets UI\n document.querySelectorAll('.preset-card').forEach(card => {\n card.classList.remove('active');\n });\n event.currentTarget.classList.add('active');\n\n // Apply expert configs\n preset.experts.forEach((exp, idx) => {\n const num = idx + 1;\n document.getElementById(`provider${num}`).value = exp.provider;\n onProviderChange(num);\n document.getElementById(`model${num}`).value = exp.model;\n updateStrengths(num);\n });\n\n // Apply chairman config\n document.getElementById('providerChairman').value = preset.chairman.provider;\n onProviderChange('Chairman');\n document.getElementById('modelChairman').value = preset.chairman.model;\n updateStrengths('Chairman');\n\n updateEstimate();\n }\n\n // ============================================\n // Estimation\n // ============================================\n function updateEstimate() {\n let totalCost = 0;\n let fastestTime = true;\n\n [1, 2, 3, 'Chairman'].forEach(num => {\n const provider = document.getElementById(`provider${num}`).value;\n const modelId = document.getElementById(`model${num}`).value;\n const model = PROVIDERS[provider]?.models.find(m => m.id === modelId);\n \n if (model) {\n totalCost += model.cost;\n if (provider !== 'groq') fastestTime = false;\n }\n });\n\n const enableReview = document.getElementById('enableReview').checked;\n if (enableReview) {\n totalCost *= 1.5;\n }\n\n // Update UI\n document.getElementById('costEstimate').textContent = totalCost === 0 ? 'Gratuit!' : `~${Math.round(totalCost)} DZD`;\n document.getElementById('timeEstimate').textContent = fastestTime ? '~5s' : (enableReview ? '~30s' : '~15s');\n \n // Show warning for expensive configs\n document.getElementById('warningBox').style.display = totalCost > 100 ? 'block' : 'none';\n }\n\n // ============================================\n // Execute Council\n // ============================================\n async function executeCouncil() {\n const query = document.getElementById('queryInput').value.trim();\n if (!query) {\n showError('Veuillez entrer une question');\n return;\n }\n\n // Get configuration\n const experts = [1, 2, 3].map(num => ({\n provider: document.getElementById(`provider${num}`).value,\n model: document.getElementById(`model${num}`).value\n }));\n \n const chairman = {\n provider: document.getElementById('providerChairman').value,\n model: document.getElementById('modelChairman').value\n };\n\n const enableReview = document.getElementById('enableReview').checked;\n\n // Show loading\n document.getElementById('loadingBox').style.display = 'block';\n document.getElementById('resultsSection').style.display = 'none';\n document.getElementById('submitBtn').disabled = true;\n\n const startTime = Date.now();\n\n try {\n // Call each expert in parallel\n document.getElementById('loadingStatus').textContent = 'Les experts analysent votre question...';\n \n // Detect language of input for instruction\n const langInstruction = '\\n\\n[IMPORTANT: Respond in the SAME language as the question above. If the question is in French, answer in French. If in English, answer in English. If in Arabic, answer in Arabic.]';\n \n const expertPromises = experts.map((exp, idx) => \n callLLM(exp.model, query + langInstruction, `Expert ${idx + 1}`)\n );\n\n const expertResponses = await Promise.all(expertPromises);\n\n // Store opinions\n currentOpinions = {};\n expertResponses.forEach((response, idx) => {\n currentOpinions[`expert${idx + 1}`] = response;\n });\n\n // Chairman synthesis\n document.getElementById('loadingStatus').textContent = 'Le Chairman synthétise les réponses...';\n \n const synthesisPrompt = `You are the Chairman of an AI expert council. \nHere is the question asked: \"${query}\"\n\nHere are the expert opinions:\n\nExpert 1 (${getModelName(experts[0].model)}):\n${expertResponses[0]}\n\nExpert 2 (${getModelName(experts[1].model)}):\n${expertResponses[1]}\n\nExpert 3 (${getModelName(experts[2].model)}):\n${expertResponses[2]}\n\nAs Chairman, provide a comprehensive synthesis that:\n1. Identifies points of consensus\n2. Notes unique perspectives from each expert\n3. Proposes a balanced and complete final answer\n\nCRITICAL: You MUST respond in the SAME LANGUAGE as the original question. If the question is in French, respond entirely in French. If in English, respond in English. If in Arabic, respond in Arabic. Match the language exactly.`;\n\n const finalResponse = await callLLM(chairman.model, synthesisPrompt, 'Chairman');\n\n // Calculate stats\n const execTime = ((Date.now() - startTime) / 1000).toFixed(1);\n\n // Display results\n displayResults({\n finalResponse,\n chairman,\n experts,\n execTime,\n opinions: currentOpinions\n });\n\n } catch (error) {\n showError(`Erreur: ${error.message}`);\n } finally {\n document.getElementById('loadingBox').style.display = 'none';\n document.getElementById('submitBtn').disabled = false;\n }\n }\n\n async function callLLM(model, prompt, role) {\n try {\n // Check if it's an Ollama model\n if (model.startsWith('ollama.')) {\n return await callOllama(model, prompt, role);\n }\n\n // Check if it's an OpenRouter model\n if (model.startsWith('openrouter.')) {\n return await callOpenRouter(model, prompt, role);\n }\n\n // Check if it's a Groq model - call directly\n if (model.startsWith('groq.')) {\n return await callGroq(model, prompt, role);\n }\n\n // Check if it's a DeepSeek model - call directly\n if (model.startsWith('deepseek.')) {\n return await callDeepSeek(model, prompt, role);\n }\n\n // Check if it's an OpenAI/ChatGPT model\n if (model.startsWith('openai.')) {\n return await callOpenAI(model, prompt, role);\n }\n\n // Check if it's an Anthropic/Claude model\n if (model.startsWith('anthropic.')) {\n return await callAnthropic(model, prompt, role);\n }\n\n // Check if it's a Perplexity model\n if (model.startsWith('perplexity.')) {\n return await callPerplexity(model, prompt, role);\n }\n\n // Check if it's a Grok/xAI model\n if (model.startsWith('grok.')) {\n return await callGrok(model, prompt, role);\n }\n\n // Check if it's a Mistral model\n if (model.startsWith('mistral.')) {\n return await callMistral(model, prompt, role);\n }\n\n // Standard API call for other providers (fallback)\n const systemPrompt = 'You MUST respond in the same language as the user input. If the user writes in French, respond in French. If in English, respond in English. Match the language exactly.';\n const response = await fetch('/api/llm/chat', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n model: model,\n messages: [\n { role: 'system', content: systemPrompt },\n { role: 'user', content: prompt }\n ],\n temperature: 0.7,\n max_tokens: 2000\n })\n });\n\n const data = await response.json();\n \n if (!response.ok || !data.success) {\n let errorMsg = 'Erreur inconnue';\n if (typeof data.detail === 'string') {\n errorMsg = data.detail;\n } else if (typeof data.error === 'string') {\n errorMsg = data.error;\n } else if (data.detail && typeof data.detail === 'object') {\n errorMsg = JSON.stringify(data.detail);\n } else if (data.error && typeof data.error === 'object') {\n errorMsg = JSON.stringify(data.error);\n }\n throw new Error(`${role}: ${errorMsg}`);\n }\n\n return data.answer || data.response || 'Réponse vide';\n } catch (fetchError) {\n if (fetchError.message.includes(role)) {\n throw fetchError;\n }\n throw new Error(`${role}: ${fetchError.message}`);\n }\n }\n\n async function callOllama(model, prompt, role) {\n // Extract actual model name (e.g., \"ollama.mistral:7b\" -> \"mistral:7b\")\n const ollamaModel = model.replace('ollama.', '');\n const systemPrompt = 'You MUST respond in the same language as the user input. Match the language exactly.';\n \n try {\n const response = await fetch('/ollama/api/generate', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n model: ollamaModel,\n prompt: prompt,\n system: systemPrompt,\n stream: false\n })\n });\n\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}`);\n }\n\n const data = await response.json();\n return data.response || 'Réponse vide';\n } catch (error) {\n throw new Error(`${role} (Ollama): ${error.message}`);\n }\n }\n\n async function callOpenRouter(model, prompt, role) {\n // Extract actual model name (e.g., \"openrouter.deepseek/deepseek-chat\" -> \"deepseek/deepseek-chat\")\n const openrouterModel = model.replace('openrouter.', '');\n \n try {\n const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${OPENROUTER_API_KEY}`,\n 'HTTP-Referer': window.location.href,\n 'X-Title': 'IAFactory Council'\n },\n body: JSON.stringify({\n model: openrouterModel,\n messages: [\n { role: 'system', content: 'You MUST respond in the same language as the user input. Match the language exactly.' },\n { role: 'user', content: prompt }\n ],\n max_tokens: 2000,\n temperature: 0.7\n })\n });\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n const errorMsg = errorData.error?.message || `HTTP ${response.status}`;\n throw new Error(errorMsg);\n }\n\n const data = await response.json();\n return data.choices?.[0]?.message?.content || 'Réponse vide';\n } catch (error) {\n throw new Error(`${role} (OpenRouter): ${error.message}`);\n }\n }\n\n async function callGroq(model, prompt, role) {\n // Extract actual model name (e.g., \"groq.llama-3.3-70b-versatile\" -> \"llama-3.3-70b-versatile\")\n const groqModel = model.replace('groq.', '');\n \n try {\n const response = await fetch('https://api.groq.com/openai/v1/chat/completions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${GROQ_API_KEY}`\n },\n body: JSON.stringify({\n model: groqModel,\n messages: [\n { role: 'system', content: 'You MUST respond in the same language as the user input. Match the language exactly.' },\n { role: 'user', content: prompt }\n ],\n max_tokens: 2000,\n temperature: 0.7\n })\n });\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n const errorMsg = errorData.error?.message || `HTTP ${response.status}`;\n throw new Error(errorMsg);\n }\n\n const data = await response.json();\n return data.choices?.[0]?.message?.content || 'Réponse vide';\n } catch (error) {\n throw new Error(`${role} (Groq): ${error.message}`);\n }\n }\n\n async function callDeepSeek(model, prompt, role) {\n // Extract actual model name (e.g., \"deepseek.deepseek-chat\" -> \"deepseek-chat\")\n const deepseekModel = model.replace('deepseek.', '');\n \n try {\n const response = await fetch('https://api.deepseek.com/v1/chat/completions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${DEEPSEEK_API_KEY}`\n },\n body: JSON.stringify({\n model: deepseekModel,\n messages: [\n { role: 'system', content: 'You MUST respond in the same language as the user input. If the user writes in French, respond in French. If in English, respond in English. Never respond in Chinese unless the user writes in Chinese.' },\n { role: 'user', content: prompt }\n ],\n max_tokens: 2000,\n temperature: 0.7\n })\n });\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n const errorMsg = errorData.error?.message || `HTTP ${response.status}`;\n throw new Error(errorMsg);\n }\n\n const data = await response.json();\n return data.choices?.[0]?.message?.content || 'Réponse vide';\n } catch (error) {\n throw new Error(`${role} (DeepSeek): ${error.message}`);\n }\n }\n\n // ============================================\n // OpenAI / ChatGPT API\n // ============================================\n async function callOpenAI(model, prompt, role) {\n if (!OPENAI_API_KEY) {\n throw new Error('Clé API OpenAI non configurée. Ajoutez votre clé dans OPENAI_API_KEY');\n }\n const openaiModel = model.replace('openai.', '');\n \n try {\n const response = await fetch('https://api.openai.com/v1/chat/completions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${OPENAI_API_KEY}`\n },\n body: JSON.stringify({\n model: openaiModel,\n messages: [\n { role: 'system', content: 'You MUST respond in the same language as the user input. Match the language exactly.' },\n { role: 'user', content: prompt }\n ],\n max_tokens: 2000,\n temperature: 0.7\n })\n });\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n const errorMsg = errorData.error?.message || `HTTP ${response.status}`;\n throw new Error(errorMsg);\n }\n\n const data = await response.json();\n return data.choices?.[0]?.message?.content || 'Réponse vide';\n } catch (error) {\n throw new Error(`${role} (OpenAI): ${error.message}`);\n }\n }\n\n // ============================================\n // Anthropic / Claude API\n // ============================================\n async function callAnthropic(model, prompt, role) {\n if (!ANTHROPIC_API_KEY) {\n throw new Error('Clé API Anthropic non configurée. Ajoutez votre clé dans ANTHROPIC_API_KEY');\n }\n const claudeModel = model.replace('anthropic.', '');\n \n try {\n const response = await fetch('https://api.anthropic.com/v1/messages', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'x-api-key': ANTHROPIC_API_KEY,\n 'anthropic-version': '2023-06-01',\n 'anthropic-dangerous-direct-browser-access': 'true'\n },\n body: JSON.stringify({\n model: claudeModel,\n max_tokens: 2000,\n system: 'You MUST respond in the same language as the user input. Match the language exactly.',\n messages: [\n { role: 'user', content: prompt }\n ]\n })\n });\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n const errorMsg = errorData.error?.message || `HTTP ${response.status}`;\n throw new Error(errorMsg);\n }\n\n const data = await response.json();\n return data.content?.[0]?.text || 'Réponse vide';\n } catch (error) {\n throw new Error(`${role} (Claude): ${error.message}`);\n }\n }\n\n // ============================================\n // Perplexity API\n // ============================================\n async function callPerplexity(model, prompt, role) {\n if (!PERPLEXITY_API_KEY) {\n throw new Error('Clé API Perplexity non configurée. Ajoutez votre clé dans PERPLEXITY_API_KEY');\n }\n const perplexityModel = model.replace('perplexity.', '');\n \n try {\n const response = await fetch('https://api.perplexity.ai/chat/completions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${PERPLEXITY_API_KEY}`\n },\n body: JSON.stringify({\n model: perplexityModel,\n messages: [\n { role: 'system', content: 'You MUST respond in the same language as the user input. Match the language exactly.' },\n { role: 'user', content: prompt }\n ],\n max_tokens: 2000,\n temperature: 0.7\n })\n });\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n const errorMsg = errorData.error?.message || `HTTP ${response.status}`;\n throw new Error(errorMsg);\n }\n\n const data = await response.json();\n return data.choices?.[0]?.message?.content || 'Réponse vide';\n } catch (error) {\n throw new Error(`${role} (Perplexity): ${error.message}`);\n }\n }\n\n // ============================================\n // Grok / xAI API\n // ============================================\n async function callGrok(model, prompt, role) {\n if (!GROK_API_KEY) {\n throw new Error('Clé API Grok/xAI non configurée. Ajoutez votre clé dans GROK_API_KEY');\n }\n const grokModel = model.replace('grok.', '');\n \n try {\n const response = await fetch('https://api.x.ai/v1/chat/completions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${GROK_API_KEY}`\n },\n body: JSON.stringify({\n model: grokModel,\n messages: [\n { role: 'system', content: 'You MUST respond in the same language as the user input. Match the language exactly.' },\n { role: 'user', content: prompt }\n ],\n max_tokens: 2000,\n temperature: 0.7\n })\n });\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n const errorMsg = errorData.error?.message || `HTTP ${response.status}`;\n throw new Error(errorMsg);\n }\n\n const data = await response.json();\n return data.choices?.[0]?.message?.content || 'Réponse vide';\n } catch (error) {\n throw new Error(`${role} (Grok): ${error.message}`);\n }\n }\n\n // ============================================\n // Mistral API\n // ============================================\n async function callMistral(model, prompt, role) {\n if (!MISTRAL_API_KEY) {\n throw new Error('Clé API Mistral non configurée. Ajoutez votre clé dans MISTRAL_API_KEY');\n }\n const mistralModel = model.replace('mistral.', '');\n \n try {\n const response = await fetch('https://api.mistral.ai/v1/chat/completions', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${MISTRAL_API_KEY}`\n },\n body: JSON.stringify({\n model: mistralModel,\n messages: [\n { role: 'system', content: 'You MUST respond in the same language as the user input. Match the language exactly.' },\n { role: 'user', content: prompt }\n ],\n max_tokens: 2000,\n temperature: 0.7\n })\n });\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n const errorMsg = errorData.error?.message || `HTTP ${response.status}`;\n throw new Error(errorMsg);\n }\n\n const data = await response.json();\n return data.choices?.[0]?.message?.content || 'Réponse vide';\n } catch (error) {\n throw new Error(`${role} (Mistral): ${error.message}`);\n }\n }\n\n function getModelName(modelId) {\n for (const provider of Object.values(PROVIDERS)) {\n const model = provider.models.find(m => m.id === modelId);\n if (model) return model.name;\n }\n return modelId;\n }\n\n // ============================================\n // Display Results\n // ============================================\n function displayResults(data) {\n document.getElementById('resultsSection').style.display = 'block';\n document.getElementById('finalResponse').textContent = data.finalResponse;\n document.getElementById('chairmanName').textContent = `(${getModelName(data.chairman.model)})`;\n document.getElementById('execTime').innerHTML = `<i class=\"fas fa-clock\"></i> ${data.execTime}s`;\n document.getElementById('actualCost').innerHTML = `<i class=\"fas fa-coins\"></i> Estimé`;\n document.getElementById('expertsCount').innerHTML = `<i class=\"fas fa-users\"></i> 3 experts + Chairman`;\n\n // Build opinion tabs\n const tabsHTML = data.experts.map((exp, idx) => `\n <button class=\"opinion-tab ${idx === 0 ? 'active' : ''}\" \n onclick=\"showOpinion('expert${idx + 1}', this)\">\n Expert ${idx + 1} (${getModelName(exp.model)})\n </button>\n `).join('');\n\n document.getElementById('opinionTabs').innerHTML = tabsHTML;\n showOpinion('expert1', document.querySelector('.opinion-tab'));\n\n // Scroll to results\n document.getElementById('resultsSection').scrollIntoView({ behavior: 'smooth' });\n }\n\n function showOpinion(expertKey, tabElement) {\n // Update tabs\n document.querySelectorAll('.opinion-tab').forEach(tab => tab.classList.remove('active'));\n tabElement.classList.add('active');\n\n // Show content\n document.getElementById('opinionContent').textContent = currentOpinions[expertKey] || 'Opinion non disponible';\n }\n\n // ============================================\n // Error Handling\n // ============================================\n function showError(message) {\n const toast = document.getElementById('errorToast');\n document.getElementById('errorMessage').textContent = message;\n toast.classList.add('visible');\n\n setTimeout(() => {\n toast.classList.remove('visible');\n }, 5000);\n }\n\n // Listen for review checkbox\n document.getElementById('enableReview').addEventListener('change', updateEstimate);\n </script>\n<!-- ========== CHATBOT HELP FLOTTANT - À inclure avant \n<div id=\"cb-help\" style=\"position: fixed; bottom: 24px; right: 24px; z-index: 9999;\">\n <button onclick=\"document.getElementById('cb-fr').style.display = document.getElementById('cb-fr').style.display === 'block' ? 'none' : 'block'\" style=\"width: 60px; height: 60px; border-radius: 50%; background: linear-gradient(135deg, #00a651, #00d66a); border: none; cursor: pointer; color: white; font-size: 24px;\">?</button>\n <div id=\"cb-fr\" style=\"display: none; position: fixed; bottom: 100px; right: 24px; width: 380px; height: 600px; border-radius: 16px; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); background: white; overflow: hidden;\">\n <iframe src=\"../chatbot-ia/index.html\" style=\"width: 100%; height: 100%; border: none;\"></iframe>\n </div>\n</div>\n <script src=\"/apps/shared/iafactory-unified.js\"></script>\n <div data-iaf-footer></div>\n\\1 ========== -->\n</div>\n\n<script>\n// ============================================\n \n if (!response.ok) throw new Error('Erreur API');\n \n const data = await response.json();\n const botResponse = data.choices?.[0]?.message?.content || 'Désolé, je n\\'ai pas pu répondre.';\n \n } catch (error) {\n const messagesContainer = document.getElementById('helpMessages');\n if (messagesContainer && messagesContainer.lastChild) {\n messagesContainer.removeChild(messagesContainer.lastChild);\n }\n }\n}\n\n const messagesContainer = document.getElementById('helpMessages');\n if (!messagesContainer) return;\n const messageDiv = document.createElement('div');\n messageDiv.className = `help-message help-${sender}`;\n \n const avatar = sender === 'bot'\n ? `<div style=\"width: 32px; height: 32px; background: linear-gradient(135deg, #00843D 0%, #2ecc71 100%); color: white; border-radius: 8px; display: flex; align-items: center; justify-content: center; font-weight: bold; font-size: 11px;\">DZ</div>`\n : '👤';\n\n messageDiv.innerHTML = `\n <div class=\"help-avatar\">${avatar}</div>\n `;\n \n messagesContainer.appendChild(messageDiv);\n messagesContainer.scrollTop = messagesContainer.scrollHeight;\n}\n\n}\n</script>\n\n<style>\n position: fixed;\n bottom: 24px;\n right: 24px;\n z-index: 9999;\n display: flex;\n align-items: center;\n gap: 12px;\n}\n\n.help-label {\n background: #00A651;\n color: white;\n padding: 10px 16px;\n border-radius: 20px;\n font-size: 14px;\n font-weight: 500;\n white-space: nowrap;\n box-shadow: 0 4px 15px rgba(0, 166, 81, 0.3);\n opacity: 0;\n transform: translateX(20px);\n transition: all 0.3s ease;\n pointer-events: none;\n}\n\n opacity: 1;\n transform: translateX(0);\n}\n\n.help-btn {\n width: 60px;\n height: 60px;\n border-radius: 50%;\n background: #00A651;\n border: none;\n cursor: pointer;\n font-size: 28px;\n box-shadow: 0 4px 20px rgba(0, 166, 81, 0.4);\n transition: all 0.3s;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.help-btn:hover {\n transform: scale(1.1);\n box-shadow: 0 6px 30px rgba(0, 166, 81, 0.6);\n}\n\n.help-window {\n position: absolute;\n bottom: 80px;\n right: 0;\n width: 340px;\n height: 480px;\n background: var(--bg-card, #0f172a);\n border: 1px solid var(--border-color, rgba(255,255,255,0.12));\n border-radius: 16px;\n box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);\n display: none;\n flex-direction: column;\n overflow: hidden;\n}\n\n.help-window.open {\n display: flex;\n}\n\n.help-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 16px;\n background: #00A651;\n color: white;\n}\n\n.help-header h3 {\n margin: 0;\n font-size: 18px;\n}\n\n.close-help-btn {\n background: none;\n border: none;\n color: white;\n font-size: 24px;\n cursor: pointer;\n}\n\n.help-messages {\n flex: 1;\n overflow-y: auto;\n padding: 16px;\n display: flex;\n flex-direction: column;\n gap: 12px;\n}\n\n.help-message {\n display: flex;\n gap: 8px;\n align-items: flex-start;\n}\n\n.help-message.help-user {\n flex-direction: row-reverse;\n}\n\n.help-avatar {\n width: 32px;\n height: 32px;\n border-radius: 50%;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 16px;\n flex-shrink: 0;\n}\n\n max-width: 75%;\n padding: 10px 14px;\n border-radius: 12px;\n font-size: 14px;\n line-height: 1.4;\n}\n\n background: var(--border-color, rgba(255,255,255,0.1));\n color: var(--text-primary, #f8fafc);\n border-bottom-left-radius: 4px;\n}\n\n background: #00A651;\n color: white;\n border-bottom-right-radius: 4px;\n}\n\n.help-input {\n display: flex;\n padding: 12px;\n gap: 8px;\n border-top: 1px solid var(--border-color, rgba(255,255,255,0.12));\n background: var(--bg-card, #0f172a);\n}\n\n.help-input input {\n flex: 1;\n padding: 10px 14px;\n border: 1px solid var(--border-color, rgba(255,255,255,0.12));\n border-radius: 20px;\n background: var(--input-bg, #1e293b);\n color: var(--input-text, #f8fafc);\n font-size: 14px;\n}\n\n.help-input input:focus {\n outline: none;\n border-color: #00A651;\n}\n\n.help-send-btn {\n width: 40px;\n height: 40px;\n border-radius: 50%;\n background: #00A651;\n border: none;\n color: white;\n font-size: 18px;\n cursor: pointer;\n transition: all 0.2s;\n}\n\n.help-send-btn:hover {\n background: #008c44;\n}\n\n@media (max-width: 768px) {\n bottom: 20px;\n right: 15px;\n }\n .help-window {\n width: calc(100vw - 30px);\n height: 60vh;\n bottom: 80px;\n right: -5px;\n }\n .help-btn {\n width: 56px;\n height: 56px;\n font-size: 24px;\n }\n}\n</style>\n\n\n<div id=\"cb-help\" style=\"position: fixed; bottom: 24px; right: 24px; z-index: 9999;\">\n <button onclick=\"document.getElementById('cb-fr').style.display = document.getElementById('cb-fr').style.display === 'block' ? 'none' : 'block'\" style=\"width: 60px; height: 60px; border-radius: 50%; background: linear-gradient(135deg, #00a651, #00d66a); border: none; cursor: pointer; color: white; font-size: 24px;\">?</button>\n <div id=\"cb-fr\" style=\"display: none; position: fixed; bottom: 100px; right: 24px; width: 380px; height: 600px; border-radius: 16px; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); background: white; overflow: hidden;\">\n <iframe src=\"../chatbot-ia/index.html\" style=\"width: 100%; height: 100%; border: none;\"></iframe>\n </div>\n</div>\n\n <div style=\"display:flex;padding:8px;gap:4px;background:rgba(0,0,0,0.2);\">\n <button class=\"iaf-chatbot-mode active\" id=\"iaf-mode-chat\" onclick=\"IAFactory.setChatMode('chat')\" style=\"flex:1;padding:8px;border:none;background:var(--accent,#00a651);color:white;cursor:pointer;border-radius:8px;font-size:12px;\" data-i18n=\"chatbotModeChat\">Chat IA</button>\n <button class=\"iaf-chatbot-mode\" id=\"iaf-mode-rag\" onclick=\"IAFactory.setChatMode('rag')\" style=\"flex:1;padding:8px;border:none;background:transparent;color:inherit;cursor:pointer;border-radius:8px;font-size:12px;opacity:0.6;\" data-i18n=\"chatbotModeRag\">Recherche</button>\n <button class=\"iaf-chatbot-mode\" id=\"iaf-mode-support\" onclick=\"IAFactory.setChatMode('support')\" style=\"flex:1;padding:8px;border:none;background:transparent;color:inherit;cursor:pointer;border-radius:8px;font-size:12px;opacity:0.6;\" data-i18n=\"chatbotModeSupport\">Support</button>\n </div>\n <div class=\"iaf-chatbot-messages\" id=\"iaf-chatbot-messages\" style=\"flex:1;overflow-y:auto;padding:16px;\">\n <div style=\"display:flex;gap:8px;margin-bottom:12px;\">\n <div style=\"width:32px;height:32px;background:linear-gradient(135deg,#00a651,#00d66a);color:white;border-radius:8px;display:flex;align-items:center;justify-content:center;font-weight:bold;font-size:11px;flex-shrink:0;\">DZ</div>\n <div style=\"background:rgba(0,0,0,0.2);padding:10px 14px;border-radius:12px;max-width:80%;font-size:14px;line-height:1.4;\" data-i18n=\"chatbotWelcome\" data-i18n-html>👋 Bonjour ! Je suis <strong>Dzir IA</strong>. Comment puis-je vous aider ?</div>\n </div>\n </div>\n <div style=\"display:flex;padding:12px;gap:8px;border-top:1px solid rgba(255,255,255,0.1);\">\n <input type=\"text\" id=\"iaf-chatbot-input\" placeholder=\"Tapez votre message...\" data-i18n-placeholder=\"chatbotPlaceholder\" onkeypress=\"if(event.key==='Enter')IAFactory.sendChatMessage()\" style=\"flex:1;padding:10px 14px;border:1px solid rgba(255,255,255,0.1);border-radius:20px;background:rgba(0,0,0,0.2);color:inherit;outline:none;font-size:14px;\">\n <button onclick=\"IAFactory.sendChatMessage()\" style=\"width:40px;height:40px;border-radius:50%;border:none;background:#00a651;color:white;cursor:pointer;font-size:16px;\">➤</button>\n </div>\n </div>\n\n<button class=\"iaf-chatbot-btn\" onclick=\"IAFactory.toggleChatbot()\" title=\"Aide\" aria-label=\"Aide\">💬</button>\n</body>\n</html>\n"}}, "docs/testing/TEST_COORDINATION.md": {"7d6e298b582d583185cd24b914c17d9daeaa9491": {"t": 1786068479.6222162, "n": 7704, "h": "fe57d12e66fc3d50", "c": "# 🚀 Test du Système de Coordination BMAD → Archon → Bolt.DIY\n\n## ✅ Nouveau système mis en place\n\n### Architecture\n```\nBolt.DIY Chat ──┐\n ├──> BMAD Agents (19 agents)\n │ │\n │ ▼\n │ Coordination API\n │ │\n │ ├──> Analyse conversation\n │ ├──> Création projet Archon\n │ ├──> Knowledge base depuis transcript\n │ └──> Lancement Bolt.DIY avec contexte\n │\n └──> Archon MCP (Port 8051)\n │\n └──> RAG Knowledge Base\n```\n\n## 🧪 Test Manual avec cURL\n\n### 1. Test simple - Analyser une conversation\n\n```bash\ncurl -X POST http://localhost:8180/api/coordination/analyze-conversation \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"messages\": [\n {\n \"role\": \"user\",\n \"content\": \"Je veux créer une application de chat en temps réel avec React et Node.js\",\n \"agent\": \"User\"\n },\n {\n \"role\": \"assistant\",\n \"content\": \"Excellente idée! Pour une app de chat temps réel, je recommande: 1) Frontend React avec TypeScript 2) Backend Node.js + Socket.io 3) Base de données MongoDB 4) Redis pour le cache\",\n \"agent\": \"Winston (Architect)\"\n },\n {\n \"role\": \"user\",\n \"content\": \"Ok parfait. Il faut aussi ajouter authentification JWT et stockage des messages\",\n \"agent\": \"User\"\n }\n ],\n \"agents_used\": [\"bmm-architect\", \"bmm-dev\"],\n \"auto_create_project\": false\n }' | python -m json.tool\n```\n\n**Résultat attendu:**\n- `is_project`: true\n- `project_name`: \"Chat\" (ou similaire)\n- `technologies`: [\"react\", \"node\", \"mongodb\", \"redis\"]\n- `requirements`: Liste des exigences détectées\n\n### 2. Test complet - Créer projet automatiquement\n\n```bash\ncurl -X POST http://localhost:8180/api/coordination/create-project \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"messages\": [\n {\n \"role\": \"user\",\n \"content\": \"Je veux développer une plateforme e-commerce avec panier, paiement Stripe et gestion produits\",\n \"agent\": \"User\"\n },\n {\n \"role\": \"assistant\",\n \"content\": \"Pour votre plateforme e-commerce, voici l'\\''architecture: 1) Frontend: React + TailwindCSS 2) Backend: FastAPI Python 3) Base: PostgreSQL 4) Paiement: Stripe API\",\n \"agent\": \"Winston\"\n },\n {\n \"role\": \"user\",\n \"content\": \"Besoin aussi de recherche produits performante et système de recommandations\",\n \"agent\": \"User\"\n },\n {\n \"role\": \"assistant\",\n \"content\": \"Pour la recherche, j'\\''intègre Elasticsearch. Pour les recommandations, algorithme collaborative filtering avec Redis cache\",\n \"agent\": \"Amelia\"\n }\n ],\n \"agents_used\": [\"bmm-architect\", \"bmm-dev\", \"bmm-analyst\"],\n \"auto_create_project\": true\n }' | python -m json.tool\n```\n\n**Résultat attendu:**\n```json\n{\n \"success\": true,\n \"project_id\": \"project_1234567890\",\n \"knowledge_source_id\": \"source_project_1234567890\",\n \"bolt_url\": \"http://localhost:5173?project_id=project_1234567890&knowledge_source=source_project_1234567890\",\n \"archon_project_url\": \"http://localhost:8180/projects/project_1234567890\",\n \"analysis\": {\n \"is_project\": true,\n \"project_name\": \"E-commerce\",\n \"technologies\": [\"react\", \"python\", \"postgresql\", \"redis\", \"stripe\"]\n }\n}\n```\n\n### 3. Test finalisation - Générer commande Bolt\n\n```bash\ncurl -X POST \"http://localhost:8180/api/coordination/finalize-and-launch?project_id=project_123&knowledge_source_id=source_123\" \\\n | python -m json.tool\n```\n\n**Résultat:**\n- `bolt_url`: URL directe pour Bolt.DIY\n- `bolt_command`: Commande shell pour lancer\n- `instructions`: Liste des étapes suivantes\n\n## 📊 Workflow Complet Utilisateur\n\n### Scénario: Créer une app depuis zéro avec BMAD → Bolt\n\n1. **L'utilisateur ouvre Bolt.DIY**\n - URL: http://localhost:5173\n - Interface chat avec sélecteur d'agents BMAD\n\n2. **Conversation avec agents BMAD**\n ```\n User: \"Je veux créer une app de gestion de tâches collaborative\"\n\n [Sélectionne Winston - Architect]\n Winston: \"Parfait! Voici l'architecture que je propose...\"\n\n [Sélectionne John - Product Manager]\n John: \"Voici les features prioritaires et la roadmap...\"\n\n [Sélectionne Amelia - Developer]\n Amelia: \"Je commence par le backend FastAPI avec...\"\n ```\n\n3. **Détection automatique du projet**\n - Le système détecte qu'un projet se dessine\n - Proposition: \"Voulez-vous créer un projet Archon depuis cette conversation?\"\n\n4. **Création automatique**\n - ✅ Projet créé dans Archon\n - ✅ Knowledge base peuplée avec transcript\n - ✅ Contexte technique extrait\n - ✅ URL Bolt.DIY générée avec contexte\n\n5. **Lancement Bolt.DIY avec contexte**\n - Bolt s'ouvre avec le projet pré-configuré\n - Accès aux agents BMAD via MCP\n - Accès à la knowledge base Archon via RAG\n - Peut commencer à coder directement\n\n## 🔧 Configuration Requise\n\n### Variables d'environnement backend (.env)\n\n```bash\n# Coordination\nARCHON_API_URL=http://localhost:8180\nBOLT_DIY_URL=http://localhost:5173\n\n# DeepSeek pour agents BMAD\nDEEPSEEK_API_KEY=sk-e2d7d214600946479856ffafbe1ce392\n\n# Archon MCP\nMCP_SERVER_URL=http://localhost:8051\n```\n\n## 🎯 Prochaines Étapes\n\n### À implémenter dans Bolt.DIY:\n\n1. **Sélecteur d'agents BMAD dans UI**\n ```tsx\n <AgentSelector\n agents={bmadAgents}\n onSelect={(agent) => setCurrentAgent(agent)}\n />\n ```\n\n2. **Client MCP dans Bolt**\n ```typescript\n // Connexion MCP pour accès Archon\n const mcpClient = new MCPClient('http://localhost:8051');\n\n // Recherche RAG depuis chat\n const results = await mcpClient.call('archon:rag_search_knowledge_base', {\n query: userMessage\n });\n ```\n\n3. **Intégration coordination API**\n ```typescript\n // Quand projet détecté, créer dans Archon\n const result = await fetch('/api/coordination/create-project', {\n method: 'POST',\n body: JSON.stringify({\n messages: conversationHistory,\n agents_used: usedAgents,\n auto_create_project: true\n })\n });\n ```\n\n## 📝 Exemples de conversations qui créent un projet\n\n### Exemple 1: Application mobile\n```\nUser: \"Je veux une app mobile pour suivi de fitness\"\nAgent: \"React Native + Firebase + Stripe\"\n→ Projet créé: \"Fitness Tracker\"\n→ Technologies: react-native, firebase, stripe\n```\n\n### Exemple 2: API backend\n```\nUser: \"Besoin d'une API REST pour gestion d'inventaire\"\nAgent: \"FastAPI + PostgreSQL + Redis cache\"\n→ Projet créé: \"Inventory API\"\n→ Technologies: python, fastapi, postgresql, redis\n```\n\n### Exemple 3: Dashboard analytics\n```\nUser: \"Dashboard temps réel pour analytics business\"\nAgent: \"Next.js + Chart.js + WebSocket + TimescaleDB\"\n→ Projet créé: \"Analytics Dashboard\"\n→ Technologies: nextjs, websocket, timescaledb\n```\n\n## 🚨 Notes importantes\n\n- ✅ **Backend coordination**: Port 8180, route `/api/coordination/*`\n- ✅ **MCP Archon**: Port 8051, outils disponibles\n- ⏳ **Bolt.DIY**: À configurer avec client MCP\n- ⏳ **UI sélection agents**: À ajouter dans Bolt\n- ✅ **DeepSeek API**: Fonctionnel pour tous agents BMAD\n\n## 📊 Monitoring\n\n### Vérifier santé du système:\n\n```bash\n# Backend coordination\ncurl http://localhost:8180/api/coordination/health\n\n# Agents BMAD\ncurl http://localhost:8180/api/bmad/agents | python -m json.tool\n\n# Chat BMAD\ncurl http://localhost:8180/api/bmad/chat/health\n\n# MCP Archon\ncurl http://localhost:8051/health\n```\n\n---\n\n**🎉 Le système de coordination est prêt!**\n\nLes agents BMAD peuvent maintenant créer automatiquement des projets Archon avec knowledge base, prêts à être développés dans Bolt.DIY.\n"}}}}