Twan07's picture
Upload 3 files
6eda9c9 verified
<style>
#gemini-fab {
position: fixed;
bottom: 25px;
right: 25px;
width: 60px;
height: 60px;
background-color: #4285f4;
border-radius: 50%;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.25);
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
z-index: 1000;
transition:
transform 0.2s ease-in-out,
box-shadow 0.2s ease-in-out,
width 0.2s ease,
height 0.2s ease;
}
#gemini-fab:hover {
transform: scale(1.1);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
}
#gemini-fab img {
width: 38px;
height: 38px;
transition:
width 0.2s ease,
height 0.2s ease;
}
#gemini-chat-container {
position: fixed;
background-color: #ffffff;
border: 1px solid #dadce0;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
display: none;
flex-direction: column;
overflow: hidden;
z-index: 999;
font-family: 'Roboto', 'Segoe UI', Arial, sans-serif;
transition:
width 0.3s ease,
height 0.3s ease,
bottom 0.3s ease,
right 0.3s ease,
left 0.3s ease;
width: 400px;
max-height: 600px;
bottom: 95px;
right: 25px;
}
#gemini-chat-container.visible {
display: flex;
animation: slideUpFadeIn 0.3s ease-out;
}
@keyframes slideUpFadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.gemini-chat-header {
background-color: #f1f3f4;
padding: 12px 18px;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #e0e0e0;
flex-shrink: 0;
}
.gemini-chat-header h3 {
margin: 0;
font-size: 1.05rem;
color: #202124;
font-weight: 500;
}
.gemini-chat-header .close-chat-btn {
background: none;
border: none;
font-size: 1.5rem;
font-weight: 300;
line-height: 1;
cursor: pointer;
color: #5f6368;
padding: 0;
}
.gemini-chat-header .close-chat-btn:hover {
color: #202124;
}
#gemini-chat-log {
flex-grow: 1;
padding: 15px 18px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 12px;
background-color: #f8f9fa;
}
.chat-message {
padding: 10px 15px;
padding-bottom: 35px; /* Space for the bottom copy button */
border-radius: 18px;
max-width: 85%;
line-height: 1.45;
font-size: 0.92rem;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
position: relative;
}
.chat-message span {
white-space: pre-wrap;
word-wrap: break-word;
}
.chat-message pre {
background: #1e1e1e;
padding: 0.8em 1em;
overflow: auto;
border-radius: 5px;
color: #ccc;
margin: 0.5em 0;
font-size: 0.85em;
line-height: 1.5;
position: relative;
white-space: pre;
word-wrap: normal;
}
.chat-message pre code,
.chat-message pre code.hljs {
font-family: 'monospace';
display: block;
padding: 0;
}
.chat-message.user {
background-color: #d1eaff;
color: #004085;
align-self: flex-end;
border-bottom-right-radius: 6px;
}
.chat-message.ai {
background-color: #e9ecef;
color: #383d41;
align-self: flex-start;
border-bottom-left-radius: 6px;
}
.copy-code-btn {
position: absolute;
top: 8px;
right: 8px;
background-color: rgba(80, 80, 80, 0.7);
color: white;
border: none;
padding: 4px 8px;
font-size: 0.75em;
border-radius: 4px;
cursor: pointer;
font-family: sans-serif;
z-index: 1;
opacity: 0.7;
visibility: visible;
transition:
opacity 0.2s,
background-color 0.2s;
}
.copy-msg-btn {
position: absolute;
bottom: 6px;
right: 8px;
background-color: rgba(100, 100, 100, 0.6); /* Slightly different base */
color: white;
border: none;
border-radius: 50%;
cursor: pointer;
font-family: sans-serif;
z-index: 1;
opacity: 0.6;
visibility: visible;
transition:
opacity 0.2s,
background-color 0.2s;
display: inline-flex;
align-items: center;
justify-content: center;
width: 26px;
height: 26px;
padding: 0;
}
.copy-msg-btn svg {
width: 14px;
height: 14px;
fill: currentColor;
}
.copy-code-btn:hover {
background-color: rgba(100, 100, 100, 0.9);
opacity: 1;
}
.copy-msg-btn:hover {
background-color: rgba(80, 80, 80, 0.9); /* Darker on hover */
opacity: 1;
}
.copy-code-btn.copied,
.copy-msg-btn.copied {
background-color: #28a745;
opacity: 1;
}
#gemini-typing-indicator {
display: flex;
align-items: center;
padding: 8px 18px 4px 18px;
background-color: #f8f9fa;
height: 20px;
flex-shrink: 0;
}
#gemini-typing-indicator span {
height: 8px;
width: 8px;
background-color: #909090;
border-radius: 50%;
display: inline-block;
margin: 0 2px;
animation: geminiDotsBounce 1.3s infinite ease-in-out;
}
#gemini-typing-indicator span:nth-child(2) {
animation-delay: -1.1s;
}
#gemini-typing-indicator span:nth-child(3) {
animation-delay: -0.9s;
}
@keyframes geminiDotsBounce {
0%,
60%,
100% {
transform: scale(0.4);
}
30% {
transform: scale(1);
}
}
#gemini-chat-input-form {
display: flex;
padding: 12px 15px;
border-top: 1px solid #e0e0e0;
background-color: #ffffff;
align-items: center;
flex-shrink: 0;
}
#gemini-chat-input-form input[type='text'] {
flex-grow: 1;
padding: 12px 18px;
border: 1px solid #dfe1e5;
border-radius: 24px;
margin-right: 10px;
font-size: 0.95rem;
outline: none;
transition:
border-color 0.2s,
box-shadow 0.2s;
}
#gemini-chat-input-form input[type='text']:focus {
border-color: #4285f4;
box-shadow: 0 0 0 2px rgba(66, 133, 244, 0.2);
}
#gemini-chat-input-form input[type='text']:disabled {
background-color: #e9ecef;
cursor: not-allowed;
}
#gemini-chat-input-form button {
background-color: #4285f4;
color: white;
border: none;
border-radius: 50%;
width: 44px;
height: 44px;
padding: 0;
cursor: pointer;
font-size: 1.3rem;
display: flex;
align-items: center;
justify-content: center;
transition:
background-color 0.2s ease,
width 0.2s ease,
height 0.2s ease;
flex-shrink: 0;
}
#gemini-chat-input-form button:hover {
background-color: #3367d6;
}
#gemini-chat-input-form button:disabled {
background-color: #a1c6ff;
cursor: not-allowed;
}
#gemini-chat-input-form button svg {
width: 22px;
height: 22px;
fill: white;
transition:
width 0.2s ease,
height 0.2s ease;
}
@media (max-width: 991.98px) {
#gemini-chat-container {
width: 380px;
max-height: 550px;
}
}
@media (max-width: 767.98px) {
#gemini-fab {
width: 55px;
height: 55px;
bottom: 20px;
right: 20px;
}
#gemini-fab img {
width: 32px;
height: 32px;
}
#gemini-chat-container {
width: 360px;
max-height: 75vh;
bottom: 85px;
right: 20px;
}
}
@media (max-width: 575.98px) {
#gemini-fab {
width: 50px;
height: 50px;
bottom: 15px;
right: 15px;
}
#gemini-fab img {
width: 28px;
height: 28px;
}
#gemini-chat-container {
left: 10px;
right: 10px;
width: auto;
bottom: 75px;
max-height: calc(100vh - 95px);
}
.chat-message pre {
font-size: 0.8em;
padding: 0.6em 0.8em;
}
.copy-code-btn {
font-size: 0.7em;
padding: 3px 6px;
top: 5px;
right: 5px;
}
.copy-msg-btn {
width: 24px;
height: 24px;
bottom: 5px;
right: 5px;
}
.copy-msg-btn svg {
width: 12px;
height: 12px;
}
.gemini-chat-header {
padding: 10px 12px;
}
.gemini-chat-header h3 {
font-size: 0.98rem;
}
.gemini-chat-header .close-chat-btn {
font-size: 1.3rem;
}
#gemini-chat-log {
padding: 10px 12px;
gap: 8px;
}
.chat-message {
font-size: 0.88rem;
padding: 8px 12px;
padding-bottom: 30px;
max-width: calc(100% - 10px);
border-radius: 15px;
}
.chat-message.user {
border-bottom-right-radius: 4px;
}
.chat-message.ai {
border-bottom-left-radius: 4px;
}
#gemini-chat-input-form {
padding: 8px 10px;
}
#gemini-chat-input-form input[type='text'] {
padding: 10px 15px;
font-size: 0.9rem;
margin-right: 8px;
}
#gemini-chat-input-form button {
width: 40px;
height: 40px;
}
#gemini-chat-input-form button svg {
width: 18px;
height: 18px;
}
#gemini-typing-indicator {
padding: 6px 12px 2px 12px;
}
}
</style>
<div id="gemini-fab" title="Chat with Gemini">
<img
src="https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTYnHRL-tMaj6h2CYK5Yy4ixuXfuohG8g8J4g&s"
alt="Gemini AI Logo"
/>
</div>
<div id="gemini-chat-container">
<div class="gemini-chat-header">
<h3>Gemini Assistant</h3>
<button class="close-chat-btn" aria-label="Close chat">&times;</button>
</div>
<div id="gemini-chat-log">
<div class="chat-message ai">Hello! How can I assist you today?</div>
</div>
<div id="gemini-typing-indicator" class="gemini-typing-indicator" style="display: none">
<span></span><span></span><span></span>
</div>
<form id="gemini-chat-input-form">
<input type="text" id="gemini-user-input" placeholder="Type a message..." autocomplete="off" />
<button type="submit" title="Send message">
<svg viewBox="0 0 24 24">
<path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"></path>
</svg>
</button>
</form>
</div>
<script>
(function () {
const fab = document.getElementById('gemini-fab');
const chatContainer = document.getElementById('gemini-chat-container');
const chatLog = document.getElementById('gemini-chat-log');
const inputForm = document.getElementById('gemini-chat-input-form');
const userInput = document.getElementById('gemini-user-input');
const sendButton = inputForm.querySelector('button[type="submit"]');
const closeChatBtn = chatContainer.querySelector('.close-chat-btn');
const typingIndicator = document.getElementById('gemini-typing-indicator');
const TYPEWRITER_SPEED = 0.3;
let currentConversationId = null;
const SVG_COPY_ICON = `
<svg xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 24 24" width="1em" fill="currentColor">
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/>
</svg>`;
const SVG_CHECK_ICON = `
<svg xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 24 24" width="1em" fill="currentColor">
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/>
</svg>`;
fab.addEventListener('click', () => {
chatContainer.classList.toggle('visible');
if (chatContainer.classList.contains('visible')) {
userInput.focus();
}
});
closeChatBtn.addEventListener('click', () => {
chatContainer.classList.remove('visible');
});
inputForm.addEventListener('submit', async function (event) {
event.preventDefault();
const messageText = userInput.value.trim();
if (messageText === '' || userInput.disabled) return;
userInput.disabled = true;
if (sendButton) sendButton.disabled = true;
await addMessageToLog(messageText, 'user');
userInput.value = '';
showTypingIndicator(true);
try {
let apiUrl;
const isFirstMessageInSession = currentConversationId === null;
if (isFirstMessageInSession) {
apiUrl = `https://api-improve-production.up.railway.app/gemini/chat?q=${encodeURIComponent(messageText)}`;
} else {
apiUrl = `https://api-improve-production.up.railway.app/gemini/chat?q=${encodeURIComponent(messageText)}&id=${currentConversationId}`;
}
const response = await fetch(apiUrl);
const data = await response.json();
if (!response.ok) {
const errorMessage = data?.error?.message || data?.message || `Error: ${response.status}`;
await addMessageToLog(`Sorry, I couldn't get a response. ${errorMessage}`, 'ai');
return;
}
const reply = data.reply;
if (isFirstMessageInSession && data.id) {
currentConversationId = data.id;
} else if (!isFirstMessageInSession && data.id && data.id !== currentConversationId) {
currentConversationId = data.id;
}
if (reply !== undefined && reply !== null) {
await addMessageToLog(reply, 'ai');
} else {
await addMessageToLog('Sorry, I received an empty or malformed reply.', 'ai');
}
} catch (error) {
console.error('API/Network Error:', error);
await addMessageToLog(
'Oops! Something went wrong. Please check connection and try again.',
'ai'
);
} finally {
showTypingIndicator(false);
userInput.disabled = false;
if (sendButton) sendButton.disabled = false;
userInput.focus();
}
});
function showTypingIndicator(show) {
typingIndicator.style.display = show ? 'flex' : 'none';
}
function escapeHTML(str) {
const p = document.createElement('p');
p.textContent = str;
return p.innerHTML;
}
function isScrolledToBottom(element) {
const threshold = 10;
return element.scrollHeight - element.scrollTop - element.clientHeight < threshold;
}
async function typeSegment(
targetElement,
textToType,
isCode,
codeElementForHighlight,
postTypeCallback
) {
return new Promise((resolve) => {
let i = 0;
targetElement.textContent = '';
function typeChar() {
if (i < textToType.length) {
targetElement.textContent += textToType.charAt(i);
i++;
if (isScrolledToBottom(chatLog)) {
chatLog.scrollTop = chatLog.scrollHeight;
}
setTimeout(typeChar, TYPEWRITER_SPEED);
} else {
if (isCode && codeElementForHighlight && typeof hljs !== 'undefined') {
try {
hljs.highlightElement(codeElementForHighlight);
} catch (e) {
console.error('Highlight.js error:', e);
}
}
if (postTypeCallback) postTypeCallback();
resolve();
}
}
if (textToType.length === 0) {
if (postTypeCallback) postTypeCallback();
resolve();
return;
}
typeChar();
});
}
async function addMessageToLog(text, sender) {
const messageDiv = document.createElement('div');
messageDiv.classList.add('chat-message', sender);
if (sender === 'ai') {
messageDiv.style.opacity = 0;
}
const segmentsToProcess = [];
const codeBlockRegex = /```(\w*)\n([\s\S]*?)```/g;
let lastIndex = 0;
let match;
while ((match = codeBlockRegex.exec(text)) !== null) {
const textBefore = text.substring(lastIndex, match.index);
if (textBefore.trim()) {
segmentsToProcess.push({ type: 'text', content: textBefore });
}
const language = match[1].trim().toLowerCase();
const codeContent = match[2];
segmentsToProcess.push({ type: 'code', content: codeContent, lang: language });
lastIndex = codeBlockRegex.lastIndex;
}
const textAfter = text.substring(lastIndex);
if (textAfter.trim()) {
segmentsToProcess.push({ type: 'text', content: textAfter });
}
if (segmentsToProcess.length === 0 && text.trim()) {
segmentsToProcess.push({ type: 'text', content: text });
}
chatLog.appendChild(messageDiv);
if (sender === 'ai' && segmentsToProcess.length > 0) {
messageDiv.style.opacity = 1;
for (const segment of segmentsToProcess) {
const contentToProcess = segment.content.trim();
if (segment.type === 'text') {
const span = document.createElement('span');
messageDiv.appendChild(span);
await typeSegment(span, contentToProcess, false, null, () => {
span.innerHTML = escapeHTML(span.textContent);
});
} else if (segment.type === 'code') {
const preElement = document.createElement('pre');
const codeElement = document.createElement('code');
if (segment.lang) {
codeElement.className = `language-${segment.lang}`;
}
preElement.appendChild(codeElement);
const copyCodeBtn = document.createElement('button');
copyCodeBtn.className = 'copy-code-btn';
copyCodeBtn.textContent = 'Copy';
preElement.appendChild(copyCodeBtn);
copyCodeBtn.addEventListener('click', () => {
navigator.clipboard
.writeText(codeElement.textContent)
.then(() => {
copyCodeBtn.textContent = 'Copied!';
copyCodeBtn.classList.add('copied');
setTimeout(() => {
copyCodeBtn.textContent = 'Copy';
copyCodeBtn.classList.remove('copied');
}, 2000);
})
.catch((err) => {
console.error('Copy code failed', err);
copyCodeBtn.textContent = 'Error';
setTimeout(() => {
copyCodeBtn.textContent = 'Copy';
}, 2000);
});
});
messageDiv.appendChild(preElement);
await typeSegment(codeElement, contentToProcess, true, codeElement, null);
}
}
} else {
segmentsToProcess.forEach((segment) => {
const contentToRender = segment.content.trim();
if (segment.type === 'text') {
const span = document.createElement('span');
span.innerHTML = escapeHTML(contentToRender);
messageDiv.appendChild(span);
} else if (segment.type === 'code') {
const preElement = document.createElement('pre');
const codeElement = document.createElement('code');
if (segment.lang) {
codeElement.className = `language-${segment.lang}`;
}
codeElement.textContent = contentToRender;
preElement.appendChild(codeElement);
const copyCodeBtn = document.createElement('button');
copyCodeBtn.className = 'copy-code-btn';
copyCodeBtn.textContent = 'Copy';
preElement.appendChild(copyCodeBtn);
copyCodeBtn.addEventListener('click', () => {
navigator.clipboard
.writeText(codeElement.textContent)
.then(() => {
copyCodeBtn.textContent = 'Copied!';
copyCodeBtn.classList.add('copied');
setTimeout(() => {
copyCodeBtn.textContent = 'Copy';
copyCodeBtn.classList.remove('copied');
}, 2000);
})
.catch((err) => {
console.error('Copy code failed', err);
copyCodeBtn.textContent = 'Error';
setTimeout(() => {
copyCodeBtn.textContent = 'Copy';
}, 2000);
});
});
messageDiv.appendChild(preElement);
if (typeof hljs !== 'undefined') {
try {
hljs.highlightElement(codeElement);
} catch (e) {
console.error(e);
}
}
}
});
if (sender === 'ai') messageDiv.style.opacity = 1;
}
let canAddCopyMsgButton = true;
if (
messageDiv.childNodes.length === 1 &&
messageDiv.firstChild &&
messageDiv.firstChild.nodeName === 'PRE'
) {
canAddCopyMsgButton = false;
}
if (text.trim() && canAddCopyMsgButton) {
const copyMsgButton = document.createElement('button');
copyMsgButton.className = 'copy-msg-btn';
copyMsgButton.title = 'Copy message';
copyMsgButton.innerHTML = SVG_COPY_ICON;
messageDiv.appendChild(copyMsgButton);
copyMsgButton.addEventListener('click', (e) => {
e.stopPropagation();
navigator.clipboard
.writeText(text)
.then(() => {
copyMsgButton.innerHTML = SVG_CHECK_ICON;
copyMsgButton.classList.add('copied');
setTimeout(() => {
copyMsgButton.innerHTML = SVG_COPY_ICON;
copyMsgButton.classList.remove('copied');
}, 2000);
})
.catch((err) => {
console.error('Copy msg failed', err);
copyMsgButton.innerHTML = SVG_COPY_ICON;
alert('Failed to copy message.');
});
});
}
if (isScrolledToBottom(chatLog) || sender === 'user') {
chatLog.scrollTop = chatLog.scrollHeight;
}
}
})();
</script>