Okay, here is a standalone HTML file that incorporates all the requested features for a Gemini Pro chat application.
Key Features Implemented:
localStorage
.localStorage
.marked.js
for Gemini's responses.<textarea>
.highlight.js
with automatic language detection.Important Notes:
localStorage
is convenient for personal use but is not secure if others can access your browser or if the code were hosted publicly. Be careful with your key.marked.min.js
and highlight.min.js
files and potentially the CSS theme, then change the <script>
and <link>
tags to point to local files. For simplicity and direct usability, this version uses CDNs.totalTokenCount
includes both input and output tokens for the current turn, plus system instructions if any. The history contributes to the input tokens.gemini_chat.html
(Save this code as an HTML file)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gemini Pro Chat</title>
<!-- highlight.js CSS (choose a theme) -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/default.min.css">
<style>
:root {
--user-bg: #e1f5fe;
--gemini-bg: #f1f8e9;
--border-color: #ccc;
--control-bg: #f5f5f5;
--text-color: #333;
--error-color: #d32f2f;
--button-bg: #4CAF50;
--button-text: white;
}
html, body {
height: 100%;
margin: 0;
padding: 0;
font-family: sans-serif;
background-color: #e0e0e0; /* Give body a slight background */
color: var(--text-color);
display: flex;
flex-direction: column;
}
#app-container {
display: flex;
flex-direction: column;
height: 100vh; /* Full viewport height */
width: 100%;
max-width: 1200px; /* Optional: Max width for very large screens */
margin: 0 auto; /* Center the container */
background-color: white;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
}
#chatbox {
flex-grow: 1; /* Takes up available space */
overflow-y: auto;
padding: 15px;
border-bottom: 1px solid var(--border-color);
background-color: #ffffff;
scroll-behavior: smooth;
}
.message {
margin-bottom: 15px;
padding: 10px 15px;
border-radius: 8px;
max-width: 90%;
word-wrap: break-word; /* Ensure long words break */
line-height: 1.5;
}
.user-message {
background-color: var(--user-bg);
margin-left: auto; /* Align to right */
text-align: left; /* Keep text left-aligned within the bubble */
border-bottom-right-radius: 0;
}
.gemini-message {
background-color: var(--gemini-bg);
margin-right: auto; /* Align to left */
border-bottom-left-radius: 0;
}
/* Styling for Markdown content */
.gemini-message h1, .gemini-message h2, .gemini-message h3 { margin-top: 0.8em; margin-bottom: 0.4em; }
.gemini-message p { margin-top: 0; margin-bottom: 0.8em; }
.gemini-message ul, .gemini-message ol { margin-bottom: 0.8em; padding-left: 25px;}
.gemini-message li { margin-bottom: 0.3em; }
.gemini-message code:not(pre code) { /* Inline code */
background-color: #eee;
padding: 0.2em 0.4em;
border-radius: 3px;
font-size: 85%;
}
.gemini-message pre { /* Code blocks */
background-color: #2d2d2d; /* Dark background for code */
color: #f8f8f2; /* Light text for code */
padding: 15px;
border-radius: 5px;
overflow-x: auto; /* Allow horizontal scrolling for long code lines */
margin-bottom: 1em;
}
.gemini-message pre code {
background-color: transparent; /* Override inline style */
padding: 0;
font-size: 90%;
line-height: 1.4;
}
.gemini-message blockquote {
margin: 1em 0;
padding-left: 1em;
border-left: 4px solid #ccc;
color: #666;
}
.gemini-message table {
border-collapse: collapse;
margin-bottom: 1em;
width: auto; /* Don't force full width */
}
.gemini-message th, .gemini-message td {
border: 1px solid #ccc;
padding: 8px 12px;
text-align: left;
}
.gemini-message th {
background-color: #f2f2f2;
font-weight: bold;
}
.gemini-message hr {
border: none;
border-top: 1px solid #eee;
margin: 1em 0;
}
.gemini-message a {
color: #007bff;
text-decoration: none;
}
.gemini-message a:hover {
text-decoration: underline;
}
#input-area {
display: flex;
padding: 10px;
border-top: 1px solid var(--border-color);
background-color: white; /* Ensure consistent background */
}
#userInput {
flex-grow: 1;
min-height: 40px; /* Start height */
max-height: 150px; /* Max height before scrolling */
padding: 10px;
border: 1px solid var(--border-color);
border-radius: 5px;
margin-right: 10px;
resize: vertical; /* Allow vertical resize */
font-size: 1em;
line-height: 1.4;
}
#sendButton {
padding: 0 25px; /* Make button wider */
height: auto; /* Adjust height based on content/padding */
min-height: 40px; /* Match textarea initial height */
align-self: flex-end; /* Align to bottom if textarea grows */
background-color: var(--button-bg);
color: var(--button-text);
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 1em;
transition: background-color 0.2s;
}
#sendButton:hover {
background-color: #45a049;
}
#sendButton:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
#controls {
padding: 8px 15px;
background-color: var(--control-bg);
border-top: 1px solid var(--border-color);
display: flex;
flex-wrap: wrap; /* Allow wrapping on small screens */
gap: 15px; /* Spacing between control items */
align-items: center;
font-size: 0.9em;
}
.control-group {
display: flex;
align-items: center;
gap: 5px;
}
#controls label {
white-space: nowrap; /* Prevent label text wrapping */
}
#controls input[type="text"],
#controls input[type="password"],
#controls input[type="number"],
#controls select {
padding: 5px 8px;
border: 1px solid #ccc;
border-radius: 3px;
font-size: 0.95em;
}
#controls input[type="checkbox"] {
margin-right: 3px;
cursor: pointer;
}
#controls button {
padding: 5px 10px;
background-color: #6c757d;
color: white;
border: none;
border-radius: 3px;
cursor: pointer;
font-size: 0.9em;
transition: background-color 0.2s;
}
#controls button:hover {
background-color: #5a6268;
}
#clearHistoryButton {
background-color: #dc3545;
}
#clearHistoryButton:hover {
background-color: #c82333;
}
.loading-indicator {
text-align: center;
padding: 10px;
font-style: italic;
color: #666;
}
.error-message {
color: var(--error-color);
background-color: #ffcdd2;
border: 1px solid var(--error-color);
}
.translation-block {
margin-top: 15px;
padding-top: 10px;
border-top: 1px dashed #aaa;
font-size: 0.95em;
color: #555;
}
.translation-block strong {
color: #333;
}
</style>
</head>
<body>
<div id="app-container">
<div id="controls">
<div class="control-group">
<label for="apiKey">API Key:</label>
<input type="password" id="apiKey" placeholder="Enter Gemini API Key">
<button id="saveApiKeyButton">Save</button>
<span id="apiKeyStatus"></span>
</div>
<div class="control-group">
<label for="tokenThreshold">Token Alert:</label>
<input type="number" id="tokenThreshold" min="1" value="3000">
<button id="saveTokenThresholdButton">Set</button>
</div>
<div class="control-group">
<label for="enableTranslation">Translate:</label>
<input type="checkbox" id="enableTranslation">
<select id="translateLang">
<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="control-group">
<button id="clearHistoryButton">Clear History</button>
</div>
</div>
<div id="chatbox">
<!-- Messages will be appended here -->
</div>
<div id="input-area">
<textarea id="userInput" placeholder="Enter your message (Shift+Enter for new line)" rows="1"></textarea>
<button id="sendButton" disabled>Send</button>
</div>
</div>
<!-- Marked.js for Markdown parsing -->
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<!-- highlight.js for code syntax highlighting -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script>
const GEMINI_API_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent?key=";
const TRANSLATE_API_URL = "https://translate.googleapis.com/translate_a/single?client=gtx&dt=t"; // sl=auto&tl={targetLang}&q={text}
// DOM Elements
const chatbox = document.getElementById('chatbox');
const userInput = document.getElementById('userInput');
const sendButton = document.getElementById('sendButton');
const apiKeyInput = document.getElementById('apiKey');
const saveApiKeyButton = document.getElementById('saveApiKeyButton');
const apiKeyStatus = document.getElementById('apiKeyStatus');
const clearHistoryButton = document.getElementById('clearHistoryButton');
const tokenThresholdInput = document.getElementById('tokenThreshold');
const saveTokenThresholdButton = document.getElementById('saveTokenThresholdButton');
const enableTranslationCheckbox = document.getElementById('enableTranslation');
const translateLangSelect = document.getElementById('translateLang');
// State Variables
let apiKey = '';
let conversationHistory = []; // Stores { role: 'user'/'model', parts: [{ text: '...' }] }
let tokenThreshold = 3000;
let translationEnabled = false;
let translateLang = 'zh-CN';
let isLoading = false;
// --- Initialization ---
function initializeApp() {
loadApiKey();
loadConversationHistory();
loadTokenThreshold();
loadTranslationSettings();
renderHistory();
updateSendButtonState();
// Configure marked.js to use highlight.js
marked.setOptions({
highlight: function(code, lang) {
const language = hljs.getLanguage(lang) ? lang : 'plaintext';
return hljs.highlight(code, { language }).value;
},
breaks: true, // Convert single line breaks to <br>
gfm: true // Enable GitHub Flavored Markdown
});
// Event Listeners
saveApiKeyButton.addEventListener('click', saveApiKey);
apiKeyInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') saveApiKey(); });
sendButton.addEventListener('click', sendMessage);
userInput.addEventListener('keydown', handleUserInputKeydown);
clearHistoryButton.addEventListener('click', clearConversation);
saveTokenThresholdButton.addEventListener('click', saveTokenThreshold);
tokenThresholdInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') saveTokenThreshold(); });
enableTranslationCheckbox.addEventListener('change', handleTranslationToggle);
translateLangSelect.addEventListener('change', handleLanguageChange);
}
// --- Local Storage Functions ---
function saveApiKey() {
const key = apiKeyInput.value.trim();
if (key) {
localStorage.setItem('geminiApiKey', key);
apiKey = key;
apiKeyStatus.textContent = ' (Saved)';
apiKeyStatus.style.color = 'green';
apiKeyInput.value = ''; // Clear input after saving
updateSendButtonState();
console.log('API Key saved.');
} else {
apiKeyStatus.textContent = ' (Invalid)';
apiKeyStatus.style.color = 'red';
}
}
function loadApiKey() {
const savedKey = localStorage.getItem('geminiApiKey');
if (savedKey) {
apiKey = savedKey;
apiKeyStatus.textContent = ' (Loaded)';
apiKeyStatus.style.color = 'green';
} else {
apiKeyStatus.textContent = ' (Not Set)';
apiKeyStatus.style.color = 'orange';
}
}
function saveConversationHistory() {
localStorage.setItem('geminiConversationHistory', JSON.stringify(conversationHistory));
console.log('History saved:', conversationHistory.length, 'items');
}
function loadConversationHistory() {
const savedHistory = localStorage.getItem('geminiConversationHistory');
if (savedHistory) {
try {
conversationHistory = JSON.parse(savedHistory);
console.log('History loaded:', conversationHistory.length, 'items');
} catch (e) {
console.error('Error parsing saved history:', e);
conversationHistory = [];
localStorage.removeItem('geminiConversationHistory'); // Clear corrupted data
}
} else {
conversationHistory = [];
}
}
function saveTokenThreshold() {
const threshold = parseInt(tokenThresholdInput.value, 10);
if (!isNaN(threshold) && threshold > 0) {
tokenThreshold = threshold;
localStorage.setItem('geminiTokenThreshold', threshold.toString());
alert(`Token alert threshold set to ${threshold}`);
} else {
alert('Please enter a valid positive number for the token threshold.');
tokenThresholdInput.value = tokenThreshold; // Reset to previous valid value
}
}
function loadTokenThreshold() {
const savedThreshold = localStorage.getItem('geminiTokenThreshold');
if (savedThreshold) {
const parsed = parseInt(savedThreshold, 10);
if (!isNaN(parsed) && parsed > 0) {
tokenThreshold = parsed;
}
}
tokenThresholdInput.value = tokenThreshold;
}
function saveTranslationSettings() {
localStorage.setItem('geminiTranslationEnabled', translationEnabled.toString());
localStorage.setItem('geminiTranslateLang', translateLang);
}
function loadTranslationSettings() {
const savedEnabled = localStorage.getItem('geminiTranslationEnabled');
const savedLang = localStorage.getItem('geminiTranslateLang');
translationEnabled = savedEnabled === 'true';
enableTranslationCheckbox.checked = translationEnabled;
if (savedLang) {
translateLang = savedLang;
translateLangSelect.value = translateLang;
} else {
translateLangSelect.value = 'zh-CN'; // Default
}
}
// --- Chat & UI Functions ---
function addMessageToChatbox(text, role, translation = null) {
const messageDiv = document.createElement('div');
messageDiv.classList.add('message', role === 'user' ? 'user-message' : 'gemini-message');
let htmlContent = '';
if (role === 'model') {
// Render Gemini message with Marked.js
try {
htmlContent = marked.parse(text);
} catch (e) {
console.error("Marked parsing error:", e);
htmlContent = escapeHtml(text); // Fallback to escaped text
}
} else {
// User message - just escape HTML to prevent injection
htmlContent = escapeHtml(text);
}
messageDiv.innerHTML = htmlContent;
// Append translation if available
if (translation && translation.text && role === 'model') {
const translationDiv = document.createElement('div');
translationDiv.classList.add('translation-block');
const langName = translateLangSelect.options[translateLangSelect.selectedIndex].text;
translationDiv.innerHTML = `<strong>Translation (${langName}):</strong><br>${escapeHtml(translation.text)}`; // Escape translated text for safety
messageDiv.appendChild(translationDiv);
}
chatbox.appendChild(messageDiv);
// Highlight code blocks within the newly added message
if (role === 'model') {
messageDiv.querySelectorAll('pre code').forEach((block) => {
try {
hljs.highlightElement(block);
} catch (e) {
console.error("Highlight.js error:", e, block);
}
});
}
scrollToBottom();
}
function renderHistory() {
chatbox.innerHTML = ''; // Clear existing messages
conversationHistory.forEach(message => {
// Assuming history only stores the text part for simplicity here
// If history stored translations, you'd need to handle that
addMessageToChatbox(message.parts[0].text, message.role);
});
scrollToBottom();
}
function clearConversation() {
if (confirm('Are you sure you want to clear the conversation history? This cannot be undone.')) {
conversationHistory = [];
localStorage.removeItem('geminiConversationHistory');
chatbox.innerHTML = ''; // Clear display
addSystemMessage("Conversation history cleared.");
console.log('History cleared.');
}
}
function handleUserInputKeydown(e) {
// Send on Enter, add newline on Shift+Enter
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault(); // Prevent default newline behavior
sendMessage();
}
// Auto-resize textarea
autoResizeTextarea(e.target);
}
function autoResizeTextarea(textarea) {
textarea.style.height = 'auto'; // Temporarily shrink
textarea.style.height = textarea.scrollHeight + 'px'; // Set to scroll height
}
function updateSendButtonState() {
sendButton.disabled = isLoading || !apiKey || userInput.value.trim() === '';
}
function showLoading(show) {
isLoading = show;
updateSendButtonState();
// Optional: Add a visual loading indicator in the chatbox
const existingIndicator = chatbox.querySelector('.loading-indicator');
if (show && !existingIndicator) {
const loadingDiv = document.createElement('div');
loadingDiv.classList.add('loading-indicator');
loadingDiv.textContent = 'Gemini is thinking...';
chatbox.appendChild(loadingDiv);
scrollToBottom();
} else if (!show && existingIndicator) {
existingIndicator.remove();
}
}
function addSystemMessage(text, isError = false) {
const messageDiv = document.createElement('div');
messageDiv.classList.add('message', 'system-message');
if (isError) {
messageDiv.classList.add('error-message');
}
messageDiv.textContent = text;
chatbox.appendChild(messageDiv);
scrollToBottom();
}
function scrollToBottom() {
chatbox.scrollTop = chatbox.scrollHeight;
}
// Basic HTML escaping
function escapeHtml(unsafe) {
if (!unsafe) return '';
return unsafe
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
// --- API Interaction ---
async function sendMessage() {
const userText = userInput.value.trim();
if (!userText || !apiKey || isLoading) {
return;
}
showLoading(true);
userInput.value = ''; // Clear input immediately
autoResizeTextarea(userInput); // Resize after clearing
updateSendButtonState(); // Disable send button while processing
// Add user message to history and display
const userMessage = { role: 'user', parts: [{ text: userText }] };
conversationHistory.push(userMessage);
addMessageToChatbox(userText, 'user');
saveConversationHistory(); // Save after adding user message
try {
const requestBody = {
contents: conversationHistory,
// Optional: Add safetySettings, generationConfig if needed
// generationConfig: {
// temperature: 0.7,
// topK: 40,
// topP: 0.95,
// maxOutputTokens: 1024,
// },
// safetySettings: [ ... ]
};
const response = await fetch(GEMINI_API_BASE_URL + apiKey, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody),
});
if (!response.ok) {
let errorText = `API Error: ${response.status} ${response.statusText}`;
try {
const errorData = await response.json();
errorText += ` - ${errorData.error?.message || 'No additional details.'}`;
} catch (e) { /* Ignore JSON parsing error */ }
throw new Error(errorText);
}
const data = await response.json();
// --- Process Gemini Response ---
if (data.candidates && data.candidates.length > 0 && data.candidates[0].content && data.candidates[0].content.parts) {
const geminiText = data.candidates[0].content.parts[0].text;
// Token Usage Check
if (data.usageMetadata && data.usageMetadata.totalTokenCount) {
const tokensUsed = data.usageMetadata.totalTokenCount;
console.log(`Tokens used (this turn): ${tokensUsed}`);
if (tokensUsed > tokenThreshold) {
alert(`Warning: Token usage (${tokensUsed}) exceeded your threshold of ${tokenThreshold}.`);
}
} else {
console.log("Token usage metadata not found in response.");
}
// Add Gemini response to history
const modelMessage = { role: 'model', parts: [{ text: geminiText }] };
conversationHistory.push(modelMessage);
saveConversationHistory(); // Save after adding model response
// Translation (if enabled)
let translationResult = null;
if (translationEnabled) {
try {
translationResult = await translateText(geminiText, translateLang);
} catch (translateError) {
console.error('Translation failed:', translateError);
addSystemMessage(`Translation failed: ${translateError.message}`, true);
}
}
// Display Gemini message (with or without translation)
addMessageToChatbox(geminiText, 'model', translationResult);
} else if (data.promptFeedback) {
// Handle cases where the prompt was blocked
const blockReason = data.promptFeedback.blockReason;
const safetyRatings = data.promptFeedback.safetyRatings.map(r => `${r.category}: ${r.probability}`).join(', ');
const errorMessage = `Request blocked due to: ${blockReason}. Safety Ratings: [${safetyRatings}]`;
addSystemMessage(errorMessage, true);
// Remove the last user message from history as it caused the block
conversationHistory.pop();
saveConversationHistory();
console.error("Gemini request blocked:", data.promptFeedback);
}
else {
console.error("Invalid response structure from Gemini:", data);
throw new Error('Received an unexpected response structure from the API.');
}
} catch (error) {
console.error('Error sending message:', error);
addSystemMessage(`Error: ${error.message}`, true);
// Attempt to remove the last user message if the API call failed fundamentally
// Check if the last message was the user message we just added
if (conversationHistory.length > 0 && conversationHistory[conversationHistory.length - 1].role === 'user') {
// Heuristic: if the error wasn't a content block, the API call might have failed before Gemini processed it.
// Let's keep the user message for retry, unless it's a clear API key error etc.
// Or, optionally remove it:
// conversationHistory.pop();
// saveConversationHistory();
}
} finally {
showLoading(false);
userInput.focus(); // Put focus back on input
}
}
// --- Translation ---
async function translateText(text, targetLang) {
if (!text) return null;
const url = `${TRANSLATE_API_URL}&sl=auto&tl=${targetLang}&q=${encodeURIComponent(text)}`;
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Translation API Error: ${response.status}`);
}
const data = await response.json();
// Extract translated text (structure is complex: nested arrays)
let translatedText = '';
if (data && data[0] && data[0].length > 0) {
data[0].forEach(segment => {
if (segment[0]) {
translatedText += segment[0];
}
});
}
if (!translatedText) {
console.warn("Translation API returned empty result:", data);
return null;
}
return { text: translatedText };
} catch (error) {
console.error("Translation fetch error:", error);
throw error; // Re-throw to be caught in sendMessage
}
}
function handleTranslationToggle() {
translationEnabled = enableTranslationCheckbox.checked;
saveTranslationSettings();
console.log(`Translation ${translationEnabled ? 'enabled' : 'disabled'}`);
}
function handleLanguageChange() {
translateLang = translateLangSelect.value;
saveTranslationSettings();
console.log(`Translation language set to: ${translateLang}`);
}
// --- Run ---
document.addEventListener('DOMContentLoaded', initializeApp);
</script>
</body>
</html>
How to Use:
gemini_chat.html
).Enter
(use Shift+Enter
for new lines within the message).This setup provides a functional chat interface directly in your browser, leveraging Gemini Pro and incorporating your specific requirements. Remember the security implications of storing the API key in local storage.