{"slug": "ai-assistant-in-a-management-panel-gemini-api-dynamic-system-prompt-from", "title": "AI Assistant in a Management Panel: Gemini API + Dynamic System Prompt from Firebase", "summary": "A developer integrated an AI chat panel into PanelControl, an internal management tool, using the Google Gemini API with a dynamic system prompt built from live Firebase data. The system answers repetitive business questions about orders, operators, leads, and bonuses by passing real-time context and a hardcoded knowledge base to the model via vanilla JavaScript. The developer encountered model availability issues and recommends verifying model names against the official documentation before integration.", "body_md": "I integrated an AI chat panel directly into **PanelControl**, the internal commercial team management tool I maintain. No external libraries, no framework: a `fetch`\n\ncall to the Gemini API with a system prompt built dynamically from live Firebase data — orders, operators, leads, bonuses — plus a static company knowledge base hardcoded in the prompt itself. All in vanilla JavaScript.\n\nPanelControl is the internal management panel used by the commercial team to track orders, leads, activations and monthly bonuses. All data lives in Firebase Realtime Database. The team asks the same repetitive questions every day: who sold the most this month? How many activations are missing to reach the bonus threshold? How does procedure X work?\n\nThe idea was to add a **✦ Ask AI** button that opens a conversation panel — same glassmorphism style already present in the panel — responding with full awareness of the business context and the current month's live data.\n\nThe key technical point: **an AI model knows nothing about your management panel. You have to build the context and pass it with every question in the system prompt.** This article documents how that was done, including the API selection process and Gemini model versioning issues.\n\nThe first evaluation was which API to use. The two main options were: **Google Gemini API** (via `generativelanguage.googleapis.com`\n\n) and **Anthropic API** (via `api.anthropic.com`\n\n).\n\n| Gemini API | Anthropic API | |\n|---|---|---|\n| Free tier | Yes (generous) | No |\n| Billing required | Yes (card on file, not charged) | Yes |\n| REST call |\n`fetch` POST |\n`fetch` POST |\n| Response path | `candidates[0].content.parts[0].text` |\n`content[0].text` |\n\nThe choice fell on Gemini for the more generous free tier for light internal use (a few dozen questions per day). An important note: Google Cloud requires a billing account even to use the free plan, but adding a card incurs no charges as long as you stay within the free tier.\n\nThe Gemini API is called with a simple `fetch`\n\nPOST. The request body contains the prompt in the `contents`\n\nfield. The response comes back in `candidates[0].content.parts[0].text`\n\n.\n\n``` js\nasync function askGemini(userMessage, systemPrompt) {\n  const GEMINI_KEY = '[GEMINI_API_KEY]';\n  const MODEL     = 'gemini-2.5-flash-lite';\n  const ENDPOINT  = `https://generativelanguage.googleapis.com/v1beta/models/${MODEL}:generateContent?key=${GEMINI_KEY}`;\n\n  const response = await fetch(ENDPOINT, {\n    method: 'POST',\n    headers: { 'Content-Type': 'application/json' },\n    body: JSON.stringify({\n      system_instruction: { parts: [{ text: systemPrompt }] },\n      contents: [{ parts: [{ text: userMessage }] }]\n    })\n  });\n\n  const data = await response.json();\n\n  // Handle model unavailability error\n  if (!response.ok) {\n    const msg = data?.error?.message || 'API Error';\n    throw new Error(msg);\n  }\n\n  return data.candidates[0].content.parts[0].text;\n}\n```\n\nThe first obstacle wasn't technical, but one of **availability**. Gemini deprecates models quickly for new accounts. The error sequence I encountered:\n\n`gemini-2.0-flash`\n\n→ `gemini-2.0-flash-lite`\n\n→ same error`gemini-2.5-flash-lite`\n\n→ ✅ working, free tier activeThe lesson: **don't trust a hardcoded model name from a tutorial.** Before integrating, always verify on [ai.google.dev/gemini-api/docs/models](https://ai.google.dev/gemini-api/docs/models) which models are available for your account and plan. The API key format matters too: Gemini keys always start with `AIzaSy`\n\n, not other prefixes.\n\nThe **✦ Ask AI** button is positioned above the existing chat button in the management panel, with a green/teal gradient to visually distinguish it from the chat's blue. On click it opens a side panel with the same glassmorphism aesthetic as the rest of the interface.\n\n``` php\n<!-- Trigger button -->\n<button id=\"aiBtn\" class=\"fab-btn ai-fab\">\n  <span class=\"fab-label\">✦ Ask AI</span>\n  <span class=\"fab-icon\">✦</span>\n</button>\n\n<!-- Conversation panel -->\n<div id=\"aiPanel\" class=\"ai-panel hidden\">\n  <div class=\"ai-panel-header\">\n    <span>✦ AI Assistant · [PROVIDER]</span>\n    <button onclick=\"closeAiPanel()\">✕</button>\n  </div>\n  <div id=\"aiMessages\" class=\"ai-messages\"></div>\n  <div class=\"ai-input-row\">\n    <textarea id=\"aiInput\" rows=\"1\" placeholder=\"Ask a question...\"></textarea>\n    <button onclick=\"sendAiMessage()\">↑</button>\n  </div>\n</div>\n```\n\nThe panel contains: a header with icon and AI provider identifier, a message area with animated typing indicator, quick suggestion chips clickable on first launch, and an auto-resize input field with Enter to send.\n\nThe most interesting part of the integration is building the system prompt. With every question, before sending the request to the API, a text block is assembled that serializes the current state of the management panel: logged-in user, current month, operator rankings with orders by category, total leads and activations, bonus thresholds.\n\nAll this data is already in memory in the management panel because Firebase loads it at startup via `onValue`\n\nlisteners. **The system prompt reads from the existing JavaScript state — no additional database calls.**\n\n``` js\nfunction buildSystemPrompt() {\n  const mese   = getCurrentMonthLabel();   // e.g. \"June 2026\"\n  const utente = sessionStorage.getItem('panelUser') || 'Unknown';\n\n  // Operator ranking with totals per category\n  const rankingText = Object.entries(state.operatori)\n    .map(([nome, dati]) =>\n      `${nome}: ${dati.totale} orders (Cat-A: ${dati.catA}, Cat-B: ${dati.catB})`\n    ).join('\\n');\n\n  return `You are an internal assistant for the commercial team of [COMPANY].\nAlways respond in a direct and professional manner.\n\n=== CURRENT CONTEXT ===\nLogged-in user: ${utente}\nReference month: ${mese}\nOpen section: ${state.sezioneAttiva}\n\n=== OPERATORS AND RESULTS ===\n${rankingText}\n\n=== MONTHLY KPIs ===\nTotal leads: ${state.leadTotali}\nActivations: ${state.attivazioni}\nCancelled: ${state.annullati}\nBonus threshold: ${state.sogliaBonus} activations\n\n=== COMPANY KNOWLEDGE ===\n[Static knowledge goes here — see next section]\n`;\n}\n```\n\nIn addition to live data, the system prompt includes a static knowledge block that doesn't change: products and pricing plans, operational procedures, team roles, glossary of internal terms.\n\n``` js\nconst STATIC_KNOWLEDGE = `\n=== PRODUCTS ===\n[PRODUCT_A]: portable POS terminal. Available plans: [BASIC_PLAN] (€0/mo, 1.20% fee),\n  [PRO_PLAN] (€12/mo, 0.95% fee), [CUSTOM_PLAN] (negotiated).\n[PRODUCT_B]: countertop fixed terminal. Available as rental only.\n[PRODUCT_C]: business account + prepaid card. Plans: Freemium (free),\n  Smart (€9/mo), Business (€25/mo).\n\n=== PROCEDURES ===\nCustomer email confirmation:\n  1. Open the [INTERNAL_PORTAL]\n  2. Search the customer by tax ID or company name\n  3. Send the verification link from the \"Communications\" section\n  4. Wait for confirmation (usually within 24h)\n\nActivation registration:\n  1. Enter the case number in the management panel\n  2. Verify status is \"Active\" on the provider portal\n  3. Update the database record with the activation date\n\n=== GLOSSARY ===\nLead: acquired commercial contact, not yet converted\nActivation: signed contract with service active on provider portal\nCancelled: case withdrawn by client or rejected by provider\n[INTERNAL_TERM_1]: digital contract signing system\n[INTERNAL_TERM_2]: CRM platform for lead management\n`;\n```\n\nThis block is written directly as a JavaScript string in the source code. It's not read from Firebase — it's part of the source. The advantage: no additional latency. The disadvantage: updating it requires a deploy.\n\nThe right balance is putting in the static block the things that **change rarely** (product structure, core procedures, glossary) and leaving to Firebase data everything that **changes daily** (who sold what, month's leads, target reached).\n\nThe Gemini API has no memory of its own. To simulate a conversation with multiple exchanges, each subsequent call must include in the `contents`\n\narray all previous messages — alternating `role: \"user\"`\n\nand `role: \"model\"`\n\n.\n\n``` js\nlet conversationHistory = []; // reset on panel open\n\nasync function sendAiMessage() {\n  const userText = document.getElementById('aiInput').value.trim();\n  if (!userText) return;\n\n  // Add the user turn to history\n  conversationHistory.push({\n    role: 'user',\n    parts: [{ text: userText }]\n  });\n\n  const response = await fetch(ENDPOINT, {\n    method: 'POST',\n    headers: { 'Content-Type': 'application/json' },\n    body: JSON.stringify({\n      system_instruction: { parts: [{ text: buildSystemPrompt() }] },\n      contents: conversationHistory  // full history on every call\n    })\n  });\n\n  const data   = await response.json();\n  const aiText = data.candidates[0].content.parts[0].text;\n\n  // Add the model response to history\n  conversationHistory.push({\n    role: 'model',\n    parts: [{ text: aiText }]\n  });\n\n  renderMessage('ai', aiText);\n}\n```\n\nNote that `buildSystemPrompt()`\n\nis called fresh on every API request — so the live Firebase data (operators, KPIs) is always up to date, even mid-conversation.\n\n**The system prompt is the product.** Response quality depends almost entirely on how well-structured the context you pass to the model is. AI isn't magic — it responds with what you give it.\n\n**Gemini models deprecate quickly.** Don't hardcode the model name without verifying current availability. Keep it in an easy-to-update constant and document which model you're using and why.\n\n**Live data + static knowledge = the right mix.** Separating what changes every day (Firebase data) from what changes rarely (procedures, glossary) keeps the prompt maintainable over time.\n\n**For internal apps, API key security is a tradeoff.** The ideal solution is a serverless proxy (e.g. Netlify Function) that hides the key from the frontend. For a management panel with restricted access and a monthly budget cap set on the provider's console, the risk is acceptable.\n\n**No library needed.** A `fetch`\n\n, some JSON and a bit of DOM manipulation are all you need to integrate an AI model into an existing vanilla JavaScript application.\n\n*Full session on my blog: roversia.it*", "url": "https://wpnews.pro/news/ai-assistant-in-a-management-panel-gemini-api-dynamic-system-prompt-from", "canonical_source": "https://dev.to/androve2k/ai-assistant-in-a-management-panel-gemini-api-dynamic-system-prompt-from-firebase-4c8k", "published_at": "2026-06-29 08:14:00+00:00", "updated_at": "2026-06-29 08:27:18.137467+00:00", "lang": "en", "topics": ["artificial-intelligence", "large-language-models", "developer-tools"], "entities": ["Google Gemini API", "Firebase", "PanelControl", "Gemini", "Anthropic API", "Google Cloud"], "alternates": {"html": "https://wpnews.pro/news/ai-assistant-in-a-management-panel-gemini-api-dynamic-system-prompt-from", "markdown": "https://wpnews.pro/news/ai-assistant-in-a-management-panel-gemini-api-dynamic-system-prompt-from.md", "text": "https://wpnews.pro/news/ai-assistant-in-a-management-panel-gemini-api-dynamic-system-prompt-from.txt", "jsonld": "https://wpnews.pro/news/ai-assistant-in-a-management-panel-gemini-api-dynamic-system-prompt-from.jsonld"}}