gemma-3-chat-api-2 / index.html
Pamudu13's picture
Update index.html
5622e70 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- Updated Title -->
<title>Sentry - Document Assistant</title>
<!-- jQuery -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<!-- Font Awesome for Icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" integrity="sha512-9usAa10IRO0HhonpyAIVpjrylPvoDwiPUiKdWk5t3PyolY1cOd4DSE0Ga+ri4AuTroPR5aQvXU9xC6qOPnzFeg==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<style>
/* --- CSS Variables for Theme --- */
:root {
--primary-color: #2f3136; /* Darker background */
--secondary-color: #36393f; /* Main chat container background */
--tertiary-color: #40444b; /* Input fields, message bubbles */
--text-color: #dcddde; /* Main text color (light gray) */
--text-muted-color: #b9bbbe; /* Slightly dimmer text */
--accent-color: #5865f2; /* Sentry/Discord Blurple (Updated) */
--accent-hover-color: #4e5ae0; /* Darker accent for hover */
--user-message-bg: var(--accent-color); /* User message bubble */
--assistant-message-bg: var(--tertiary-color); /* Assistant message bubble */
--input-bg: var(--tertiary-color);
--border-color: #202225; /* Darkest borders */
--success-color: #43b581; /* Green */
--error-color: #f04747; /* Red */
--font-family: 'Inter', 'Helvetica Neue', Helvetica, Arial, sans-serif;
--border-radius: 8px;
--scrollbar-thumb-color: #202225;
--scrollbar-track-color: var(--secondary-color);
}
/* --- General Body Styling --- */
body {
font-family: var(--font-family);
background-color: var(--primary-color);
color: var(--text-color);
margin: 0;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
padding: 15px; /* Reduced padding for smaller screens */
box-sizing: border-box;
}
/* --- Main Chat Container --- */
.chat-container {
background-color: var(--secondary-color);
border-radius: var(--border-radius);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
width: 100%;
max-width: 750px; /* Slightly wider */
height: 90vh; /* Adjust height as needed */
max-height: 800px; /* Max height constraint */
display: flex;
flex-direction: column;
overflow: hidden; /* Contain children */
}
/* --- Header --- */
header {
background-color: var(--primary-color);
padding: 15px 20px;
border-bottom: 1px solid var(--border-color);
text-align: center;
flex-shrink: 0; /* Prevent header from shrinking */
}
header h1 {
margin: 0;
font-size: 1.3em;
color: #fff;
font-weight: 600;
}
/* --- Upload Section --- */
.upload-section {
padding: 12px 20px; /* Reduced padding */
border-bottom: 1px solid var(--border-color);
display: flex;
align-items: center;
gap: 12px;
background-color: var(--secondary-color);
flex-shrink: 0; /* Prevent shrinking */
}
/* Hide default file input */
#pdfUpload {
display: none;
}
/* Style the label like a button */
.upload-label {
background-color: var(--accent-color);
color: white;
padding: 8px 15px;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.2s ease, opacity 0.2s ease;
font-size: 0.9em;
white-space: nowrap;
display: inline-flex;
align-items: center;
gap: 8px; /* Space between icon and text */
}
.upload-label:hover {
background-color: var(--accent-hover-color);
}
.upload-label[disabled] {
opacity: 0.6;
cursor: not-allowed;
}
#uploadStatus {
font-size: 0.85em;
color: var(--text-muted-color);
flex-grow: 1; /* Take remaining space */
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
line-height: 1.3;
}
#uploadStatus i { /* Style icons in status */
margin-right: 5px;
}
/* --- Chat Area --- */
#chat {
flex-grow: 1; /* Take available space */
overflow-y: auto; /* Enable vertical scrolling */
padding: 20px;
display: flex;
flex-direction: column;
gap: 18px; /* Increased space between messages */
}
/* Scrollbar styling (Webkit) */
#chat::-webkit-scrollbar {
width: 8px;
}
#chat::-webkit-scrollbar-track {
background: var(--scrollbar-track-color);
border-radius: 4px;
}
#chat::-webkit-scrollbar-thumb {
background-color: var(--scrollbar-thumb-color);
border-radius: 4px;
}
#chat::-webkit-scrollbar-thumb:hover {
background-color: var(--tertiary-color); /* Slightly lighter on hover */
}
/* Scrollbar styling (Firefox) */
#chat {
scrollbar-width: thin;
scrollbar-color: var(--scrollbar-thumb-color) var(--scrollbar-track-color);
}
/* --- Message Styling --- */
.message {
display: flex;
max-width: 80%; /* Max width of message bubble */
opacity: 0; /* Start hidden for animation */
animation: fadeIn 0.4s ease forwards;
position: relative; /* For potential absolute elements later */
line-height: 1.45; /* Improved readability */
}
@keyframes fadeIn {
to { opacity: 1; }
}
/* Common structure for icon + text block */
.message-inner-wrapper {
display: flex;
gap: 10px; /* Space between icon and text block */
width: 100%; /* Ensure wrapper takes full width */
}
.sender-icon {
font-size: 1.1em; /* Adjust icon size */
color: var(--text-muted-color); /* Default icon color */
margin-top: 2px; /* Fine-tune vertical alignment */
flex-shrink: 0; /* Prevent icon from shrinking */
}
.message-text-block {
display: flex;
flex-direction: column; /* Stack name and content */
flex-grow: 1; /* Allow text block to grow */
}
.sender-name {
font-weight: 600; /* Bolder name */
margin-bottom: 5px; /* Space below name */
font-size: 0.88em;
color: var(--text-muted-color);
}
.message-content {
padding: 10px 15px;
border-radius: var(--border-radius);
word-wrap: break-word;
font-size: 0.95em;
background-color: var(--assistant-message-bg); /* Default background */
color: var(--text-color);
}
/* User Message Specific Styles */
.message.user {
margin-left: auto; /* Align bubble to the right */
flex-direction: row-reverse; /* Put icon on the right */
}
.message.user .message-inner-wrapper {
flex-direction: row-reverse; /* Reverse icon and text block order */
}
.message.user .sender-icon {
color: var(--text-muted-color); /* Optional: Different user icon color */
}
.message.user .sender-name {
text-align: right; /* Align name to the right */
color: inherit; /* Use bubble text color */
}
.message.user .message-content {
background-color: var(--user-message-bg);
color: white; /* Text color for user bubble */
border-bottom-right-radius: 4px; /* Subtle shape difference */
}
/* Assistant Message Specific Styles */
.message.assistant {
margin-right: auto; /* Align bubble to the left */
}
.message.assistant .sender-icon {
color: var(--accent-color); /* Sentry icon color */
}
.message.assistant .sender-name {
color: var(--accent-color); /* Sentry name color */
}
.message.assistant .message-content {
background-color: var(--assistant-message-bg);
border-bottom-left-radius: 4px; /* Subtle shape difference */
}
/* System Message Styling (for errors, info) */
.message.system {
font-style: italic;
color: var(--text-muted-color); /* Default system text color */
text-align: center;
width: 100%;
max-width: 100%;
font-size: 0.9em;
margin: 8px 0; /* Adjust spacing */
gap: 0; /* No gap needed */
}
.message.system .message-inner-wrapper { /* System messages don't need the icon/name wrapper */
justify-content: center;
}
.message.system .message-content {
background: none;
padding: 0;
display: inline-block; /* Center the text block */
}
.message.system .message-content.error {
color: var(--error-color);
}
.message.system .message-content.info {
color: var(--success-color);
}
/* --- Typing Indicator --- */
.typing-indicator {
display: flex;
align-items: center;
padding: 0px 20px 5px 20px; /* Add padding */
opacity: 0;
transition: opacity 0.3s ease, height 0.3s ease;
height: 0; /* Start hidden */
overflow: hidden;
flex-shrink: 0; /* Prevent shrinking */
gap: 8px; /* Space between icon and text/dots */
}
.typing-indicator.visible {
opacity: 1;
height: 25px; /* Make visible */
}
.typing-indicator .sender-icon { /* Reuse sender icon style */
font-size: 0.95em;
color: var(--accent-color);
}
.typing-indicator span {
font-size: 0.88em;
color: var(--text-muted-color);
margin-right: 5px;
}
.typing-indicator .dot {
display: inline-block;
width: 6px;
height: 6px;
background-color: var(--text-muted-color);
border-radius: 50%;
margin: 0 2px;
animation: typing 1.2s infinite ease-in-out;
}
.typing-indicator .dot:nth-child(1) { animation-delay: 0.0s; }
.typing-indicator .dot:nth-child(2) { animation-delay: 0.2s; }
.typing-indicator .dot:nth-child(3) { animation-delay: 0.4s; }
@keyframes typing {
0%, 100% { transform: translateY(0); opacity: 0.5; }
50% { transform: translateY(-5px); opacity: 1; }
}
/* --- Input Area --- */
.input-area {
display: flex;
padding: 15px 20px;
border-top: 1px solid var(--border-color);
background-color: var(--secondary-color); /* Match chat bg */
flex-shrink: 0; /* Prevent shrinking */
gap: 10px; /* Space between textarea and button */
}
#userInput {
flex-grow: 1;
background-color: var(--input-bg);
color: var(--text-color);
border: 1px solid var(--border-color); /* Subtle border */
border-radius: var(--border-radius);
padding: 10px 15px;
resize: none; /* Prevent manual resizing */
font-family: var(--font-family);
font-size: 0.95em;
max-height: 120px; /* Limit growth */
box-sizing: border-box;
overflow-y: auto; /* Allow scrolling if text exceeds max height */
line-height: 1.4; /* Match message line height */
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
#userInput:focus {
outline: none;
border-color: var(--accent-color);
box-shadow: 0 0 0 1px var(--accent-color);
}
#userInput::placeholder {
color: var(--text-muted-color);
opacity: 0.8;
}
#sendButton {
background-color: var(--accent-color);
color: white;
border: none;
border-radius: var(--border-radius);
padding: 0 15px;
cursor: pointer;
font-size: 1.1em; /* Icon size */
transition: background-color 0.2s ease, opacity 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
height: 42px; /* Match input height approx */
width: 45px; /* Fixed width for the button */
flex-shrink: 0; /* Prevent button from shrinking */
}
#sendButton:hover {
background-color: var(--accent-hover-color);
}
#sendButton:disabled {
opacity: 0.5;
cursor: not-allowed;
background-color: var(--tertiary-color); /* Dim background when disabled */
}
</style>
</head>
<body>
<!-- Main Chat Container -->
<div class="chat-container">
<!-- Header -->
<header>
<h1>SentryLabs Document Assistant</h1>
</header>
<!-- PDF Upload Section -->
<div class="upload-section">
<label for="pdfUpload" class="upload-label" id="uploadLabel">
<i class="fas fa-file-pdf"></i> Choose PDF
</label>
<input type="file" id="pdfUpload" accept=".pdf" />
<span id="uploadStatus">Upload a document for analysis.</span>
<!-- Hidden button, not really used now -->
<button id="uploadButton" style="display: none;">Upload</button>
</div>
<!-- Chat Messages Area -->
<div id="chat">
<!-- Initial Greeting from Sentry -->
<div class="message assistant">
<div class="message-inner-wrapper">
<i class="fas fa-shield-alt sender-icon"></i> <!-- Sentry Icon -->
<div class="message-text-block">
<div class="sender-name">Sentry</div>
<div class="message-content">
I am Sentry, your SentryLabs assistant. Please upload a PDF document using the button above to begin our analysis.
</div>
</div>
</div>
</div>
</div>
<!-- Typing Indicator -->
<div class="typing-indicator" id="typingIndicator">
<i class="fas fa-shield-alt sender-icon"></i>
<span>Sentry is analyzing...</span>
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>
</div>
<!-- User Input Area -->
<div class="input-area">
<textarea id="userInput" placeholder="Ask Sentry about the document..." rows="1" disabled></textarea> <!-- Start disabled -->
<button id="sendButton" title="Send Message" disabled> <!-- Start disabled -->
<i class="fas fa-paper-plane"></i>
</button>
</div>
</div>
<!-- JavaScript -->
<script>
$(document).ready(function() {
// --- State Variables ---
let chatHistory = []; // Stores pairs: [[userMsg, assistantMsg], ...]
let pdfUploaded = false;
// --- DOM Elements ---
const chatBox = $('#chat');
const userInput = $('#userInput');
const sendButton = $('#sendButton');
const uploadLabel = $('#uploadLabel');
const pdfUploadInput = $('#pdfUpload');
const uploadStatus = $('#uploadStatus');
const typingIndicator = $('#typingIndicator');
// --- Helper Functions ---
// Function to add a message to the chat interface
function addMessage(sender, text, type = 'normal') {
// Sanitize text to prevent basic HTML injection, preserve newlines for <br> conversion
const sanitizedHtml = $('<div>').text(text).html().replace(/\n/g, '<br>');
let messageClass = sender; // 'user', 'assistant', or 'system'
let senderName = '';
let senderIconHtml = ''; // Icon HTML string
// Define names and icons based on sender
if (sender === 'user') {
senderName = 'You';
// Optional: User icon
// senderIconHtml = '<i class="fas fa-user sender-icon"></i>';
} else if (sender === 'assistant') {
senderName = 'Sentry';
senderIconHtml = '<i class="fas fa-shield-alt sender-icon"></i>';
}
let messageHtml;
// Handle different message types (normal, error, info)
if (type === 'error' || type === 'info') {
messageClass = 'system';
// Simple centered text for system messages
messageHtml = `<div class="message-inner-wrapper">
<div class="message-content ${type}">${sanitizedHtml}</div>
</div>`;
} else {
// Standard message structure with icon, name, content
const nameHtml = senderName ? `<div class="sender-name">${senderName}</div>` : '';
messageHtml = `
<div class="message-inner-wrapper">
${senderIconHtml}
<div class="message-text-block">
${nameHtml}
<div class="message-content">${sanitizedHtml}</div>
</div>
</div>
`;
}
// Create message element and append to chat
const messageElement = $(`<div class="message ${messageClass}">${messageHtml}</div>`);
chatBox.append(messageElement);
// Scroll to the bottom of the chat
scrollToBottom();
}
// Function to scroll chat box to the bottom
function scrollToBottom() {
chatBox.animate({ scrollTop: chatBox[0].scrollHeight }, 300);
}
// Function to show/hide typing indicator
function showTypingIndicator(show) {
if (show) {
typingIndicator.addClass('visible');
} else {
typingIndicator.removeClass('visible');
}
// Adjust scroll after potential layout shift from indicator
setTimeout(scrollToBottom, 50);
}
// Adjust textarea height dynamically based on content
function adjustTextareaHeight() {
const textarea = userInput[0];
textarea.style.height = 'auto'; // Reset height to calculate scroll height accurately
// Calculate the height needed for the content, plus a small buffer if desired
const scrollHeight = textarea.scrollHeight;
textarea.style.height = scrollHeight + 'px';
// Apply max-height constraint from CSS
const maxHeight = parseInt(userInput.css('max-height'));
if (scrollHeight > maxHeight) {
textarea.style.overflowY = 'auto'; // Show scrollbar if content exceeds max height
} else {
textarea.style.overflowY = 'hidden'; // Hide scrollbar if content fits
}
}
// --- Event Handlers ---
// Trigger hidden file input when the styled label is clicked
uploadLabel.on('click', function() {
if (!$(this).prop('disabled')) { // Only trigger if not disabled
pdfUploadInput.click();
}
});
// Handle file selection via the hidden input
pdfUploadInput.on('change', function() {
const file = this.files[0];
if (file) {
if (file.type === "application/pdf") {
uploadStatus.text(`Selected: ${file.name}`).css('color', 'var(--text-muted-color)');
uploadFile(file); // Automatically start the upload
} else {
uploadStatus.html('<i class="fas fa-exclamation-triangle"></i> Error: Please select a PDF file.').css('color', 'var(--error-color)');
this.value = ''; // Reset file input to allow re-selection of the same file if needed
}
}
});
// Handle sending a message
function sendMessage() {
const message = userInput.val().trim();
// Prevent sending empty messages or if PDF isn't uploaded
if (message === "" || !pdfUploaded) return;
// Display user's message
addMessage('user', message);
const currentUserMessage = message; // Store for history pairing
// Clear input, disable controls, show typing indicator
userInput.val('').prop('disabled', true);
sendButton.prop('disabled', true);
showTypingIndicator(true);
adjustTextareaHeight(); // Reset height after clearing
// Prepare history for the API (only completed pairs)
const historyForApi = chatHistory.slice(); // Send a copy
// Make the AJAX call to the backend
$.ajax({
url: '/ask_question',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({
message: currentUserMessage,
history: historyForApi
}),
success: function(response) {
if (response && response.response) {
// Display Sentry's response
addMessage('assistant', response.response);
// Add the completed user/assistant pair to history
chatHistory.push([currentUserMessage, response.response]);
} else {
// Handle cases where backend might return empty response field
const errorMsg = "Received an unexpected empty response from Sentry.";
addMessage('system', errorMsg, 'error');
// Record the turn with an empty assistant message for history integrity
chatHistory.push([currentUserMessage, ""]);
}
},
error: function(jqXHR, textStatus, errorThrown) {
// Display error message in chat
let errorMsg = 'An error occurred while communicating with Sentry.';
if (jqXHR.responseJSON && jqXHR.responseJSON.response) { // Check for error in 'response' key first
errorMsg = jqXHR.responseJSON.response;
} else if (jqXHR.responseJSON && jqXHR.responseJSON.error) { // Check 'error' key
errorMsg = jqXHR.responseJSON.error;
}
addMessage('system', errorMsg, 'error');
// Record the turn with an empty assistant message for history integrity
chatHistory.push([currentUserMessage, ""]);
},
complete: function() {
// Re-enable input, hide typing indicator
userInput.prop('disabled', false).focus(); // Re-enable and focus
showTypingIndicator(false);
// Re-evaluate send button state (might have typed while waiting)
sendButton.prop('disabled', userInput.val().trim() === '');
}
});
}
// Attach send handler to button click
sendButton.click(sendMessage);
// Attach send handler to Enter key press in textarea (allow Shift+Enter for newline)
userInput.keypress(function(e) {
if (e.which === 13 && !e.shiftKey) { // Enter key pressed without Shift
e.preventDefault(); // Prevent default newline behavior
sendMessage();
}
});
// Enable/disable send button based on input content and PDF status
// Also adjust textarea height on input
userInput.on('input keyup', function() {
adjustTextareaHeight();
const isEmpty = $(this).val().trim() === '';
sendButton.prop('disabled', isEmpty || !pdfUploaded);
});
// --- File Upload Function ---
function uploadFile(file) {
const formData = new FormData();
formData.append('pdf', file);
// Update UI to show uploading state
uploadStatus.html(`<i class="fas fa-spinner fa-spin"></i> Processing: ${file.name}...`).css('color', 'var(--text-muted-color)');
uploadLabel.prop('disabled', true).css('opacity', 0.6); // Disable upload button visually
userInput.prop('disabled', true); // Disable input during upload
sendButton.prop('disabled', true); // Disable send during upload
$.ajax({
url: '/upload_pdf',
type: 'POST',
data: formData,
contentType: false, // Important for FormData
processData: false, // Important for FormData
success: function(response) {
// Success: Update status, enable input/send
uploadStatus.html(`<i class="fas fa-check-circle"></i> Document ready for analysis.`).css('color', 'var(--success-color)');
pdfUploaded = true;
userInput.prop('disabled', false).attr('placeholder', 'Ask Sentry about the document...').focus();
// Enable send button only if there's text already (unlikely but possible)
sendButton.prop('disabled', userInput.val().trim() === '');
// Optional: Clear chat history if you want each PDF to start fresh
// chatHistory = [];
// chatBox.find('.message:not(:first-child)').remove(); // Remove all but initial greeting
},
error: function(jqXHR, textStatus, errorThrown) {
// Error: Show error message, keep controls disabled
let errorMsg = 'Failed to process the PDF.';
if (jqXHR.responseJSON && jqXHR.responseJSON.error) {
errorMsg = jqXHR.responseJSON.error;
} // Add more specific parsing if needed
uploadStatus.html(`<i class="fas fa-exclamation-triangle"></i> Error: ${errorMsg}`).css('color', 'var(--error-color)');
pdfUploaded = false;
userInput.prop('disabled', true).attr('placeholder', 'Upload failed. Please try again.');
sendButton.prop('disabled', true);
// Add error to chat as well
addMessage('system', `PDF processing failed: ${errorMsg}`, 'error');
},
complete: function() {
// Always re-enable the upload button regardless of outcome
uploadLabel.prop('disabled', false).css('opacity', 1);
// Clear the file input value so the user can re-upload the same file if needed after an error
pdfUploadInput.val('');
}
});
}
// --- Initial Page Load Setup ---
adjustTextareaHeight(); // Initial adjustment for placeholder
// Initial state is set directly in the HTML (disabled input/button)
});
</script>
</body>
</html>