Spaces:
Running
Running
<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> |