<!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;
}
.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">
<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>
<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>
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');
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')) || [];
apiKeyInput.value = apiKey;
tokenThresholdInput.value = tokenThreshold;
translationToggle.checked = translationEnabled;
translationLanguage.value = targetLanguage;
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
});
function loadConversationHistory() {
if (conversationHistory.length > 0) {
messagesContainer.innerHTML = '';
messagesContainer.appendChild(thinkingIndicator);
conversationHistory.forEach(message => {
if (message.role === 'user') {
addUserMessage(message.content);
} else if (message.role === 'model') {
addGeminiMessage(message.content, message.tokens, message.translatedContent);
}
});
scrollToBottom();
}
}
function addUserMessage(content) {
const messageDiv = document.createElement('div');
messageDiv.className = 'message user-message';
messageDiv.textContent = content;
messagesContainer.insertBefore(messageDiv, thinkingIndicator);
scrollToBottom();
}
function addGeminiMessage(content, tokens = null, translatedContent = null) {
const messageDiv = document.createElement('div');
messageDiv.className = 'message gemini-message';
const contentDiv = document.createElement('div');
contentDiv.className = 'gemini-content';
contentDiv.innerHTML = marked.parse(content);
messageDiv.appendChild(contentDiv);
if (tokens !== null) {
const tokenInfo = document.createElement('div');
tokenInfo.className = 'token-info';
tokenInfo.textContent = `Tokens used: ${tokens}`;
messageDiv.appendChild(tokenInfo);
if (tokens > tokenThreshold) {
tokenInfo.style.color = '#f44336';
tokenInfo.textContent += ' (exceeds threshold)';
alert(`Token usage (${tokens}) exceeds your threshold (${tokenThreshold})`);
}
}
if (translationEnabled && translatedContent) {
const translatedDiv = document.createElement('div');
translatedDiv.className = 'translated-message';
translatedDiv.innerHTML = translatedContent;
messageDiv.appendChild(translatedDiv);
}
messagesContainer.insertBefore(messageDiv, thinkingIndicator);
messageDiv.querySelectorAll('pre code').forEach((block) => {
hljs.highlightBlock(block);
});
scrollToBottom();
}
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();
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';
}
}
async function sendMessageToGemini(message) {
if (!apiKey) {
alert('Please set your Gemini API key in the settings');
return;
}
thinkingIndicator.style.display = 'block';
scrollToBottom();
try {
const history = conversationHistory.map(msg => ({
role: msg.role,
parts: [{ text: msg.content }]
}));
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();
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;
const totalTokens = data.usageMetadata ? data.usageMetadata.totalTokens : null;
if (translationEnabled) {
translatedContent = await translateText(responseText, targetLanguage);
}
addGeminiMessage(responseText, totalTokens, translatedContent);
conversationHistory.push({
role: 'user',
content: message
});
conversationHistory.push({
role: 'model',
content: responseText,
tokens: totalTokens,
translatedContent: translatedContent
});
localStorage.setItem('conversationHistory', JSON.stringify(conversationHistory));
} else {
addGeminiMessage('Sorry, I could not generate a response.');
}
} catch (error) {
thinkingIndicator.style.display = 'none';
console.error('Error:', error);
addGeminiMessage(`Error: ${error.message}`);
}
}
function autoResizeTextarea() {
userInput.style.height = 'auto';
userInput.style.height = (userInput.scrollHeight) + 'px';
const maxHeight = 200;
if (userInput.scrollHeight > maxHeight) {
userInput.style.height = maxHeight + 'px';
userInput.style.overflowY = 'auto';
} else {
userInput.style.overflowY = 'hidden';
}
}
function scrollToBottom() {
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}
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');
});
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 = '';
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);
messagesContainer.appendChild(thinkingIndicator);
}
});
loadConversationHistory();
</script>
</body>
</html>