<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI Text Generation Interface</title>
<!-- Load Tailwind CSS for utility classes -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Load Inter font family -->
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap');
body {
font-family: 'Inter', sans-serif;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background-color: #343a40; /* Dark background matching Pulse feel */
}
/* Custom CSS to ensure centered layout and rounded corners */
.card {
border-radius: 0.75rem; /* Tailwind: rounded-xl */
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); /* Tailwind: shadow-xl */
}
.btn-check:checked + .btn-outline-primary {
background-color: #5c6bc0; /* Pulse primary color */
border-color: #5c6bc0;
color: #fff;
}
.nav-link.active {
font-weight: 600;
}
</style>
<!-- Load Bootstrap CSS (Pulse Theme) -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" xintegrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<link href="https://cdn.jsdelivr.net/npm/bootswatch@5.3.3/dist/pulse/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-12 col-lg-8">
<div class="card bg-light border-0">
<div class="card-body p-4 p-md-5">
<h1 class="card-title text-center text-primary mb-4 text-3xl font-extrabold">Gemini Text Playground</h1>
<!-- Either/Or Toggle Button Group for Tabs -->
<div class="d-flex justify-content-center mb-5">
<div class="btn-group shadow-md rounded-lg" role="group" aria-label="Tab Toggles">
<input type="radio" class="btn-check" name="tab-toggle" id="tab-one-toggle" autocomplete="off" checked>
<label class="btn btn-outline-primary font-semibold px-4 py-2" for="tab-one-toggle" onclick="showTab('tabOneContent')">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-pencil-square me-2" viewBox="0 0 16 16">
<path d="M15.502 1.94a.5.5 0 0 1 0 .706L14.459 3.69l-2-2L13.502.646a.5.5 0 0 1 .707 0l1.29 1.29zm-2.5 1.78l-2-2.5 1.286 1.286zM13.84 5.394a.5.5 0 0 0-.12-.24L6.5 1.5 8 0l7.34 6.394a.5.5 0 0 0-.24.12l-1.396 1.397-2 2L13.84 5.394z"/>
<path fill-rule="evenodd" d="M1 13.5A1.5 1.5 0 0 0 2.5 15h11a1.5 1.5 0 0 0 1.5-1.5v-6a.5.5 0 0 0-1 0v6a.5.5 0 0 1-.5.5h-11a.5.5 0 0 1-.5-.5v-11a.5.5 0 0 1 .5-.5H9a.5.5 0 0 0 0-1H2.5A1.5 1.5 0 0 0 1 2.5z"/>
</svg>
Tab One: Generation Form
</label>
<input type="radio" class="btn-check" name="tab-toggle" id="tab-two-toggle" autocomplete="off">
<label class="btn btn-outline-primary font-semibold px-4 py-2" for="tab-two-toggle" onclick="showTab('tabTwoContent')">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-body-text me-2" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M0 6a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1zm1 2v5h14V8zM1 2.5a.5.5 0 0 1 .5-.5h13a.5.5 0 0 1 0 1h-13a.5.5 0 0 1-.5-.5M1 4.5a.5.5 0 0 1 .5-.5h13a.5.5 0 0 1 0 1h-13a.5.5 0 0 1-.5-.5"/>
</svg>
Tab Two: Simulated Output
</label>
</div>
</div>
<!-- Tab Content Container -->
<div id="tabContent">
<!-- Tab One: Text Generation Form -->
<div id="tabOneContent" class="tab-pane active" role="tabpanel">
<form id="generationForm">
<div class="mb-4">
<label for="promptTextarea" class="form-label font-semibold">Prompt / Query</label>
<textarea class="form-control rounded-lg shadow-sm border-2 border-gray-300 focus:border-primary focus:shadow-lg" id="promptTextarea" rows="6" placeholder="Enter your text prompt here (e.g., Write a short story about a detective robot)..." required></textarea>
</div>
<div class="row g-3 mb-4">
<div class="col-md-6">
<label for="temperatureInput" class="form-label font-semibold">Temperature (0.0 - 1.0)</label>
<input type="number" class="form-control rounded-lg shadow-sm" id="temperatureInput" min="0.0" max="1.0" step="0.1" value="0.7">
</div>
<div class="col-md-6">
<label for="maxTokensInput" class="form-label font-semibold">Max Output Length</label>
<input type="number" class="form-control rounded-lg shadow-sm" id="maxTokensInput" min="1" value="500">
</div>
</div>
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary btn-lg rounded-lg font-bold shadow-lg transition duration-150 ease-in-out hover:scale-105" id="generateButton">
<span id="buttonText">Generate Text</span>
<div id="loadingSpinner" class="spinner-border spinner-border-sm text-light hidden" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</button>
</div>
</form>
</div>
<!-- Tab Two: Simulated Text Output -->
<div id="tabTwoContent" class="tab-pane hidden" role="tabpanel">
<h3 class="text-xl font-bold text-gray-800 mb-3">Generated Content</h3>
<!-- Message Box for non-alert messages -->
<div id="messageBox" class="alert alert-warning hidden" role="alert"></div>
<div class="bg-white p-4 rounded-lg border border-gray-200 shadow-sm min-h-60">
<p id="outputText" class="text-gray-700 whitespace-pre-wrap">
Your generated text will appear here after submission. Try Tab One!
</p>
<p id="outputSource" class="mt-4 text-sm text-primary font-semibold hidden"></p>
</div>
<div class="d-grid gap-2 mt-4">
<button onclick="copyToClipboard('outputText')" class="btn btn-secondary rounded-lg font-bold transition duration-150 ease-in-out hover:scale-[1.01]">
Copy Output
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Firebase and LLM API Setup (Mandatory) -->
<script type="module">
// Import necessary Firebase modules
import { initializeApp } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-app.js";
import { getAuth, signInAnonymously, signInWithCustomToken } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-auth.js";
import { getFirestore } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-firestore.js";
// Global variables provided by the environment
const appId = typeof __app_id !== 'undefined' ? __app_id : 'default-app-id';
const firebaseConfig = typeof __firebase_config !== 'undefined' ? JSON.parse(__firebase_config) : null;
const initialAuthToken = typeof __initial_auth_token !== 'undefined' ? __initial_auth_token : null;
// --- Firebase Initialization and Auth ---
let app;
let db;
let auth;
let userId = null;
if (firebaseConfig) {
app = initializeApp(firebaseConfig);
db = getFirestore(app);
auth = getAuth(app);
// Authentication function
async function authenticate() {
try {
if (initialAuthToken) {
await signInWithCustomToken(auth, initialAuthToken);
} else {
await signInAnonymously(auth);
}
userId = auth.currentUser?.uid || 'anonymous';
console.log("Firebase Auth successful. User ID:", userId);
} catch (error) {
console.error("Firebase Auth failed:", error);
// Fallback to anonymous if custom token fails
try {
await signInAnonymously(auth);
userId = auth.currentUser?.uid || 'anonymous-fallback';
console.log("Fallback to Anonymous Auth successful. User ID:", userId);
} catch (anonError) {
console.error("Anonymous Auth also failed:", anonError);
// If all fails, use a random ID (though security rules will likely prevent access)
userId = crypto.randomUUID();
}
}
}
authenticate();
} else {
console.warn("Firebase configuration not found. Firestore operations disabled.");
}
// --- Gemini API Call Functions ---
const LLM_MODEL = 'gemini-2.5-flash-preview-05-20';
const API_KEY = ""; // Placeholder, will be injected by the environment
// Exponential backoff utility for API retries
const exponentialBackoffFetch = async (url, options, maxRetries = 5) => {
for (let i = 0; i < maxRetries; i++) {
try {
const response = await fetch(url, options);
if (response.status === 429 && i < maxRetries - 1) {
const delay = Math.pow(2, i) * 1000 + Math.random() * 1000;
console.log(`Rate limit exceeded. Retrying in ${delay / 1000}s...`);
await new Promise(resolve => setTimeout(resolve, delay));
continue;
}
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response;
} catch (error) {
if (i === maxRetries - 1) {
throw error;
}
// Handle network errors or other non-429 errors
const delay = Math.pow(2, i) * 1000 + Math.random() * 1000;
console.error(`Fetch attempt ${i + 1} failed. Retrying in ${delay / 1000}s...`, error);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
};
async function callGeminiApi(prompt, temperature, maxOutputTokens) {
const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/${LLM_MODEL}:generateContent?key=${API_KEY}`;
const payload = {
contents: [{ parts: [{ text: prompt }] }],
tools: [{ "google_search": {} }], // Enable Google Search Grounding
systemInstruction: {
parts: [{
text: "You are a creative and helpful writing assistant. Generate text based on the user's prompt. Ensure the response is well-structured and engaging."
}]
},
config: {
temperature: parseFloat(temperature),
maxOutputTokens: parseInt(maxOutputTokens)
}
};
const options = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
};
try {
const response = await exponentialBackoffFetch(apiUrl, options);
const result = await response.json();
const candidate = result.candidates?.[0];
if (candidate && candidate.content?.parts?.[0]?.text) {
const text = candidate.content.parts[0].text;
let sources = [];
const groundingMetadata = candidate.groundingMetadata;
if (groundingMetadata && groundingMetadata.groundingAttributions) {
sources = groundingMetadata.groundingAttributions
.map(attribution => ({
uri: attribution.web?.uri,
title: attribution.web?.title,
}))
.filter(source => source.uri && source.title);
}
return { text, sources };
} else if (result.error) {
console.error("API Error:", result.error);
throw new Error(result.error.message || "An unknown API error occurred.");
} else {
throw new Error("Received an unexpected response format from the API.");
}
} catch (error) {
console.error("Fetch failed:", error);
throw new Error(`Failed to connect to the AI model: ${error.message}`);
}
}
// --- UI Logic and Event Handlers ---
window.showTab = function(tabId) {
// Hide all tabs
document.querySelectorAll('.tab-pane').forEach(tab => {
tab.classList.add('hidden');
tab.classList.remove('active');
});
// Show the selected tab
const selectedTab = document.getElementById(tabId);
selectedTab.classList.remove('hidden');
selectedTab.classList.add('active');
}
const form = document.getElementById('generationForm');
const promptTextarea = document.getElementById('promptTextarea');
const temperatureInput = document.getElementById('temperatureInput');
const maxTokensInput = document.getElementById('maxTokensInput');
const outputTextElement = document.getElementById('outputText');
const outputSourceElement = document.getElementById('outputSource');
const generateButton = document.getElementById('generateButton');
const buttonText = document.getElementById('buttonText');
const loadingSpinner = document.getElementById('loadingSpinner');
const tabTwoToggle = document.getElementById('tab-two-toggle');
const messageBox = document.getElementById('messageBox');
function showMessage(type, message, duration = 5000) {
messageBox.className = `alert alert-${type}`;
messageBox.textContent = message;
messageBox.classList.remove('hidden');
setTimeout(() => {
messageBox.classList.add('hidden');
}, duration);
}
form.addEventListener('submit', async (e) => {
e.preventDefault();
const prompt = promptTextarea.value.trim();
const temp = temperatureInput.value;
const maxTokens = maxTokensInput.value;
if (!prompt) {
showMessage('danger', 'Please enter a prompt before generating text.', 3000);
return;
}
// Set loading state
generateButton.disabled = true;
buttonText.classList.add('hidden');
loadingSpinner.classList.remove('hidden');
outputTextElement.textContent = 'Generating text... Please wait.';
outputSourceElement.classList.add('hidden');
messageBox.classList.add('hidden');
try {
// 1. Call the LLM API
const result = await callGeminiApi(prompt, temp, maxTokens);
// 2. Update the output content
outputTextElement.textContent = result.text;
// 3. Handle Grounding Sources
if (result.sources && result.sources.length > 0) {
const sourceText = "Sources: " + result.sources.map(s => `<a href="${s.uri}" target="_blank" class="text-info">${s.title}</a>`).join(', ');
outputSourceElement.innerHTML = sourceText;
outputSourceElement.classList.remove('hidden');
} else {
outputSourceElement.classList.add('hidden');
}
// 4. Switch to the Output Tab
tabTwoToggle.checked = true;
showTab('tabTwoContent');
showMessage('success', 'Text generation complete!', 2000);
} catch (error) {
console.error("Generation failed:", error);
outputTextElement.textContent = `Error: ${error.message}. Please check the console for details.`;
showMessage('danger', 'Generation failed. See output for error details.', 5000);
outputSourceElement.classList.add('hidden');
} finally {
// Reset button state
generateButton.disabled = false;
buttonText.classList.remove('hidden');
loadingSpinner.classList.add('hidden');
}
});
// --- Clipboard Function ---
window.copyToClipboard = function(elementId) {
const textToCopy = document.getElementById(elementId).textContent;
// Use execCommand for broader compatibility in various browser environments (like iframes)
const tempInput = document.createElement('textarea');
tempInput.value = textToCopy;
document.body.appendChild(tempInput);
tempInput.select();
try {
document.execCommand('copy');
showMessage('info', 'Text copied to clipboard!', 2000);
} catch (err) {
console.error('Could not copy text: ', err);
showMessage('warning', 'Failed to copy text. Please select and copy manually.', 3000);
}
document.body.removeChild(tempInput);
};
// Initialize the first tab as active on load
document.addEventListener('DOMContentLoaded', () => {
showTab('tabOneContent');
});
</script>
<!-- Load Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" xintegrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
</body>
</html>