| <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; |
| 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); |
| 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); |
| 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">×</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> |
|
|