Aria OS Tabbed Interface


<!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>