HW

    Write a browser-side app that uses google's gemini pro api to chat in a standalone HTML. Requirements are listed as follows: (1) Save my Gemini key in a local storage. (2) Support the conversation history and save it a local storage. Add a button to clear the conversation history so that gemini is aware of the context of previous messages. Allow the user to set the token usage threshold, compare it with tokens used in the metadata of gemini pro api response and alert the user if beyond the thrshold. (3) Use marked.js to display Gemini messages. (4) Use the textarea element for user to paste multi-line text. (5) Use highlight.js to check the code language automatically and highlight its code block if any. (6) Add one feature that uses free Google Translate API (https://translate.googleapis.com/translate_a/single) to translate the Gemini message to any language and append the translated message to the "normal" Gemini message. Provide an HTML element for the user to enable/disable the translation feature. Use select and option elements for the user to select output language which defaults to Simplified Chinese. Languages also support Traditional Chinese, Japanese, English and Spanish. Note that the translated text may not have the same format as the input text and marked.js may not handle it well. (7) Maximize the display window. Maximize the message window and minimize control/config buttons except the send message button.

    I'll create a browser-side app that uses Google's Gemini Pro API for chatting. This will be a standalone HTML file with all the features you requested.

    This standalone HTML application provides a complete interface for chatting with Google's Gemini Pro API. Here's a breakdown of the features I've implemented:

    1. API Key Management:
      • Saves the Gemini API key in local storage
      • Input is masked for security
    2. Conversation History:
      • Automatically saves all messages in local storage
      • Includes a clear history button with confirmation
      • Preserves context between sessions by sending previous messages to Gemini
    3. Token Threshold Alerts:
      • User can set a custom token usage threshold
      • Shows token count for each Gemini response
      • Alerts when token usage exceeds the threshold
    4. Markdown Rendering:
      • Uses marked.js to render markdown in Gemini responses
      • Supports code blocks, lists, headings, etc.
    5. Input Interface:
      • Textarea for multi-line input with auto-resizing
      • Send button with keyboard shortcut (Enter)
    6. Code Highlighting:
      • Uses highlight.js for automatic language detection
      • Applies syntax highlighting to all code blocks
    7. Translation Feature:
      • Toggle to enable/disable translation
      • Language selector with Simplified Chinese as default
      • Also supports Traditional Chinese, Japanese, English, and Spanish
      • Translated content appears below the original message
    8. UI Considerations:
      • Maximized display and message windows
      • Minimal controls tucked into a settings panel
      • Responsive design that works on different screen sizes
      • Visual indicator when Gemini is thinking

    To use the app, just open the HTML file in your browser, enter your Gemini API key in the settings (gear icon), and start chatting!


    Gemini Chat Web App

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Gemini Chat</title> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/github.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> <style> :root { --primary-color: #6750A4; --background-color: #f5f5f7; --card-color: #ffffff; --text-color: #1c1c1e; --border-color: #e0e0e0; } * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; background-color: var(--background-color); color: var(--text-color); display: flex; flex-direction: column; height: 100vh; overflow: hidden; } header { background-color: var(--primary-color); color: white; padding: 10px 20px; display: flex; justify-content: space-between; align-items: center; } .title { font-size: 1.2rem; font-weight: bold; } .settings-toggle { background: none; border: none; color: white; cursor: pointer; font-size: 1.2rem; } .settings-panel { position: absolute; top: 50px; right: 0; background-color: var(--card-color); border: 1px solid var(--border-color); border-radius: 8px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); z-index: 100; padding: 15px; width: 300px; display: none; } .settings-panel.show { display: block; } .form-group { margin-bottom: 12px; } .form-group label { display: block; margin-bottom: 5px; font-weight: 500; } .form-group input, .form-group select { width: 100%; padding: 8px; border: 1px solid var(--border-color); border-radius: 4px; } .toggle-container { display: flex; align-items: center; margin-top: 5px; } .toggle-switch { position: relative; display: inline-block; width: 40px; height: 20px; margin-right: 10px; } .toggle-switch input { opacity: 0; width: 0; height: 0; } .toggle-slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; border-radius: 20px; transition: .4s; } .toggle-slider:before { position: absolute; content: ""; height: 16px; width: 16px; left: 2px; bottom: 2px; background-color: white; border-radius: 50%; transition: .4s; } input:checked + .toggle-slider { background-color: var(--primary-color); } input:checked + .toggle-slider:before { transform: translateX(20px); } button { background-color: var(--primary-color); color: white; border: none; padding: 8px 12px; border-radius: 4px; cursor: pointer; font-weight: 500; } button:hover { opacity: 0.9; } .chat-container { flex: 1; display: flex; flex-direction: column; overflow: hidden; padding: 20px; } .messages { flex: 1; overflow-y: auto; padding-right: 10px; margin-bottom: 20px; } .message { margin-bottom: 16px; padding: 12px; border-radius: 8px; max-width: 90%; white-space: pre-wrap; } .user-message { background-color: #e1f5fe; align-self: flex-end; margin-left: auto; } .gemini-message { background-color: var(--card-color); border: 1px solid var(--border-color); } .translated-message { margin-top: 10px; padding-top: 10px; border-top: 1px dashed var(--border-color); } .message-meta { font-size: 0.8rem; color: #666; margin-top: 5px; } .input-container { display: flex; gap: 10px; } #user-input { flex: 1; padding: 12px; border: 1px solid var(--border-color); border-radius: 8px; resize: none; font-family: inherit; font-size: 1rem; min-height: 60px; max-height: 200px; } #send-button { align-self: flex-end; height: 40px; } /* Markdown styling */ .gemini-content { line-height: 1.5; } .gemini-content p { margin-bottom: 1em; } .gemini-content h1, .gemini-content h2, .gemini-content h3 { margin-top: 1em; margin-bottom: 0.5em; } .gemini-content ul, .gemini-content ol { margin-left: 1.5em; margin-bottom: 1em; } .gemini-content code { background-color: #f5f5f5; padding: 0.2em 0.4em; border-radius: 3px; font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; } .gemini-content pre { background-color: #f5f5f5; padding: 1em; border-radius: 5px; overflow-x: auto; margin-bottom: 1em; } .gemini-content pre code { background-color: transparent; padding: 0; } .gemini-content blockquote { border-left: 4px solid var(--primary-color); padding-left: 1em; margin-left: 0; color: #555; } .token-info { font-size: 0.8rem; color: #666; margin-top: 5px; } #thinking { display: none; margin-bottom: 16px; } .dot-typing { position: relative; padding-left: 3px; } .dot-typing::after { content: ''; animation: dot-typing 1.5s infinite linear; display: inline-block; } @keyframes dot-typing { 0% { content: ''; } 25% { content: '.'; } 50% { content: '..'; } 75% { content: '...'; } 100% { content: ''; } } </style> </head> <body> <header> <div class="title">Gemini Chat</div> <button class="settings-toggle" id="settings-toggle"> <i class="fas fa-cog"></i> </button> </header> <div class="settings-panel" id="settings-panel"> <div class="form-group"> <label for="api-key">Gemini API Key</label> <input type="password" id="api-key" placeholder="Enter your Gemini API key"> </div> <div class="form-group"> <label for="token-threshold">Token Usage Threshold</label> <input type="number" id="token-threshold" placeholder="Enter threshold" value="4000"> </div> <div class="form-group"> <label>Translation</label> <div class="toggle-container"> <label class="toggle-switch"> <input type="checkbox" id="translation-toggle"> <span class="toggle-slider"></span> </label> <span>Enable Translation</span> </div> </div> <div class="form-group"> <label for="translation-language">Translation Language</label> <select id="translation-language"> <option value="zh-CN">Simplified Chinese</option> <option value="zh-TW">Traditional Chinese</option> <option value="ja">Japanese</option> <option value="en">English</option> <option value="es">Spanish</option> </select> </div> <div class="form-group"> <button id="clear-history">Clear Chat History</button> </div> </div> <div class="chat-container"> <div class="messages" id="messages"> <!-- Messages will be displayed here --> <div class="message gemini-message"> <div class="gemini-content"> Hi! I'm Gemini. How can I help you today? </div> </div> <div id="thinking" class="message gemini-message"> <div class="dot-typing">Thinking</div> </div> </div> <div class="input-container"> <textarea id="user-input" placeholder="Type your message..." rows="1"></textarea> <button id="send-button"> <i class="fas fa-paper-plane"></i> </button> </div> </div> <!-- Scripts --> <script src="https://cdnjs.cloudflare.com/ajax/libs/marked/4.3.0/marked.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script> <script> // DOM Elements const messagesContainer = document.getElementById('messages'); const userInput = document.getElementById('user-input'); const sendButton = document.getElementById('send-button'); const settingsToggle = document.getElementById('settings-toggle'); const settingsPanel = document.getElementById('settings-panel'); const apiKeyInput = document.getElementById('api-key'); const tokenThresholdInput = document.getElementById('token-threshold'); const translationToggle = document.getElementById('translation-toggle'); const translationLanguage = document.getElementById('translation-language'); const clearHistoryButton = document.getElementById('clear-history'); const thinkingIndicator = document.getElementById('thinking'); // Configuration let apiKey = localStorage.getItem('geminiApiKey') || ''; let tokenThreshold = parseInt(localStorage.getItem('tokenThreshold')) || 4000; let translationEnabled = localStorage.getItem('translationEnabled') === 'true' || false; let targetLanguage = localStorage.getItem('targetLanguage') || 'zh-CN'; let conversationHistory = JSON.parse(localStorage.getItem('conversationHistory')) || []; // Initialize settings apiKeyInput.value = apiKey; tokenThresholdInput.value = tokenThreshold; translationToggle.checked = translationEnabled; translationLanguage.value = targetLanguage; // Configure marked.js marked.setOptions({ highlight: function(code, lang) { if (lang && hljs.getLanguage(lang)) { return hljs.highlight(code, { language: lang }).value; } else { return hljs.highlightAuto(code).value; } }, breaks: true, gfm: true }); // Load conversation history function loadConversationHistory() { if (conversationHistory.length > 0) { // Clear the default first message messagesContainer.innerHTML = ''; // Add thinking indicator back messagesContainer.appendChild(thinkingIndicator); // Render messages from history conversationHistory.forEach(message => { if (message.role === 'user') { addUserMessage(message.content); } else if (message.role === 'model') { addGeminiMessage(message.content, message.tokens, message.translatedContent); } }); // Scroll to bottom scrollToBottom(); } } // Add user message to UI function addUserMessage(content) { const messageDiv = document.createElement('div'); messageDiv.className = 'message user-message'; messageDiv.textContent = content; // Insert before thinking indicator messagesContainer.insertBefore(messageDiv, thinkingIndicator); scrollToBottom(); } // Add Gemini message to UI function addGeminiMessage(content, tokens = null, translatedContent = null) { const messageDiv = document.createElement('div'); messageDiv.className = 'message gemini-message'; // Create main content const contentDiv = document.createElement('div'); contentDiv.className = 'gemini-content'; contentDiv.innerHTML = marked.parse(content); messageDiv.appendChild(contentDiv); // Add token info if available if (tokens !== null) { const tokenInfo = document.createElement('div'); tokenInfo.className = 'token-info'; tokenInfo.textContent = `Tokens used: ${tokens}`; messageDiv.appendChild(tokenInfo); // Check if token usage exceeds threshold if (tokens > tokenThreshold) { tokenInfo.style.color = '#f44336'; tokenInfo.textContent += ' (exceeds threshold)'; alert(`Token usage (${tokens}) exceeds your threshold (${tokenThreshold})`); } } // Add translated content if available if (translationEnabled && translatedContent) { const translatedDiv = document.createElement('div'); translatedDiv.className = 'translated-message'; translatedDiv.innerHTML = translatedContent; messageDiv.appendChild(translatedDiv); } // Insert before thinking indicator messagesContainer.insertBefore(messageDiv, thinkingIndicator); // Apply syntax highlighting messageDiv.querySelectorAll('pre code').forEach((block) => { hljs.highlightBlock(block); }); scrollToBottom(); } // Translate text using Google Translate API async function translateText(text, targetLang) { try { const response = await fetch(`https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=${targetLang}&dt=t&q=${encodeURIComponent(text)}`); const data = await response.json(); // Extract translated text from response let translatedText = ''; if (data && data[0]) { data[0].forEach(item => { if (item[0]) { translatedText += item[0]; } }); } return translatedText; } catch (error) { console.error('Translation error:', error); return 'Translation failed'; } } // Send message to Gemini API async function sendMessageToGemini(message) { if (!apiKey) { alert('Please set your Gemini API key in the settings'); return; } // Show thinking indicator thinkingIndicator.style.display = 'block'; scrollToBottom(); try { // Prepare the conversation history for Gemini const history = conversationHistory.map(msg => ({ role: msg.role, parts: [{ text: msg.content }] })); // Add the current message const currentMessage = { role: "user", parts: [{ text: message }] }; const payload = { contents: history.length > 0 ? [...history, currentMessage] : [currentMessage], generationConfig: { temperature: 0.7, topK: 40, topP: 0.95, maxOutputTokens: 8192, } }; const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent?key=${apiKey}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); const data = await response.json(); // Hide thinking indicator thinkingIndicator.style.display = 'none'; if (data.error) { addGeminiMessage(`Error: ${data.error.message}`); return; } if (data.candidates && data.candidates[0] && data.candidates[0].content) { const responseText = data.candidates[0].content.parts[0].text; let translatedContent = null; // Get token usage const totalTokens = data.usageMetadata ? data.usageMetadata.totalTokens : null; // Translate if enabled if (translationEnabled) { translatedContent = await translateText(responseText, targetLanguage); } // Add to UI addGeminiMessage(responseText, totalTokens, translatedContent); // Save to history conversationHistory.push({ role: 'user', content: message }); conversationHistory.push({ role: 'model', content: responseText, tokens: totalTokens, translatedContent: translatedContent }); // Save to localStorage localStorage.setItem('conversationHistory', JSON.stringify(conversationHistory)); } else { addGeminiMessage('Sorry, I could not generate a response.'); } } catch (error) { // Hide thinking indicator thinkingIndicator.style.display = 'none'; console.error('Error:', error); addGeminiMessage(`Error: ${error.message}`); } } // Auto-resize textarea function autoResizeTextarea() { userInput.style.height = 'auto'; userInput.style.height = (userInput.scrollHeight) + 'px'; // Limit max height const maxHeight = 200; if (userInput.scrollHeight > maxHeight) { userInput.style.height = maxHeight + 'px'; userInput.style.overflowY = 'auto'; } else { userInput.style.overflowY = 'hidden'; } } // Scroll to bottom of messages function scrollToBottom() { messagesContainer.scrollTop = messagesContainer.scrollHeight; } // Event Listeners sendButton.addEventListener('click', () => { const message = userInput.value.trim(); if (message) { addUserMessage(message); sendMessageToGemini(message); userInput.value = ''; userInput.style.height = 'auto'; } }); userInput.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendButton.click(); } }); userInput.addEventListener('input', autoResizeTextarea); settingsToggle.addEventListener('click', () => { settingsPanel.classList.toggle('show'); }); // Close settings when clicking outside document.addEventListener('click', (e) => { if (!settingsPanel.contains(e.target) && e.target !== settingsToggle) { settingsPanel.classList.remove('show'); } }); apiKeyInput.addEventListener('change', () => { apiKey = apiKeyInput.value; localStorage.setItem('geminiApiKey', apiKey); }); tokenThresholdInput.addEventListener('change', () => { tokenThreshold = parseInt(tokenThresholdInput.value); localStorage.setItem('tokenThreshold', tokenThreshold); }); translationToggle.addEventListener('change', () => { translationEnabled = translationToggle.checked; localStorage.setItem('translationEnabled', translationEnabled); }); translationLanguage.addEventListener('change', () => { targetLanguage = translationLanguage.value; localStorage.setItem('targetLanguage', targetLanguage); }); clearHistoryButton.addEventListener('click', () => { if (confirm('Are you sure you want to clear the chat history?')) { conversationHistory = []; localStorage.setItem('conversationHistory', JSON.stringify(conversationHistory)); messagesContainer.innerHTML = ''; // Add default welcome message const welcomeDiv = document.createElement('div'); welcomeDiv.className = 'message gemini-message'; welcomeDiv.innerHTML = '<div class="gemini-content">Hi! I\'m Gemini. How can I help you today?</div>'; messagesContainer.appendChild(welcomeDiv); // Add thinking indicator back messagesContainer.appendChild(thinkingIndicator); } }); // Initialize loadConversationHistory(); </script> </body> </html>
    Last edited 1 hour ago