âš™</button> <!-- Gear icon -->
</div>
<!-- Main Chat Area -->
<div id="gpt-chat-container">
<div class="warning">INSECURE: API Key visible! Client-Side File Parsing! TESTING ONLY.</div>
<div id="gpt-chat-messages">
<div class="gpt-chat-message assistant">Hello! How can I help? You can upload TXT, DOCX, XLSX, or PDF files via Settings to extract text.</div>
</div>
<div id="gpt-chat-input-area">
<input type="text" id="gpt-chat-input" placeholder="Type your message...">
<button id="gpt-chat-send">Send</button>
</div>
</div>
</div>
<!-- Settings Modal -->
<div id="gpt-settings-modal">
<div id="gpt-settings-content">
<h2>Settings</h2>
<label for="gpt-model-select">Model:</label>
<select id="gpt-model-select">
<option value="gpt-4o-mini">GPT-4o Mini</option>
<option value="gpt-4o">GPT-4o</option>
<!-- Add other CHAT models like gpt-3.5-turbo if needed -->
</select>
<label for="gpt-system-message">System Message (Optional):</label>
<textarea id="gpt-system-message" rows="3" placeholder="e.g., You are a helpful assistant analyzing document text."></textarea>
<p class="gpt-settings-note">Guides the AI's behavior. Sent first in every API request.</p>
<label for="gpt-file-upload">Upload Document for Text Extraction:</label>
<!-- Updated accept attribute -->
<input type="file" id="gpt-file-upload" accept=".txt,.pdf,.doc,.docx,.xls,.xlsx,application/pdf,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,text/plain">
<p class="gpt-settings-note">Select TXT, DOCX, XLSX, or PDF. Text will be extracted when you click 'Save Settings'. Can be slow for large/complex files.</p>
<div id="file-processing-status"></div> <!-- Renamed Status Area -->
<p class="gpt-settings-note gpt-file-warning">Accuracy varies (esp. for PDF). Older formats (.doc, .xls) may not work reliably. Large files can strain browser & exceed API limits.</p>
<div id="gpt-settings-buttons">
<button id="gpt-close-settings">Close</button>
<button id="gpt-save-settings">Save Settings</button>
</div>
</div>
</div>
<script>
// --- Configuration (INSECURE - FOR TESTING ONLY) ---
const OPENAI_API_KEY = "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"; // <--- !!! EXTREMELY INSECURE !!!
const OPENAI_API_URL = "https://api.openai.com/v1/chat/completions";
// --- DOM Elements ---
const messagesContainer = document.getElementById('gpt-chat-messages');
const inputField = document.getElementById('gpt-chat-input');
const sendButton = document.getElementById('gpt-chat-send');
const settingsOpenButton = document.getElementById('gpt-settings-open');
const settingsModal = document.getElementById('gpt-settings-modal');
const settingsCloseButton = document.getElementById('gpt-close-settings');
const settingsSaveButton = document.getElementById('gpt-save-settings');
const modelSelect = document.getElementById('gpt-model-select');
const fileInput = document.getElementById('gpt-file-upload');
const systemMessageInput = document.getElementById('gpt-system-message');
const fileProcessingStatusDiv = document.getElementById('file-processing-status'); // Renamed
// --- State ---
let conversationHistory = [];
let currentModel = modelSelect.value;
let currentSystemMessage = "";
let isFileProcessing = false; // Renamed flag
// --- Functions ---
/** Appends a message to the chat window */
function displayMessage(sender, text, type = 'message') {
const messageElement = document.createElement('div');
const senderClass = (sender === 'system-info') ? 'system-info' : sender;
messageElement.classList.add('gpt-chat-message', senderClass);
if (type === 'error') {
messageElement.classList.add('error');
messageElement.textContent = Error: ${String(text).split('\n')[0]};
console.error("Detailed Error:", text);
} else if (type === 'loading') {
messageElement.classList.add('loading');
messageElement.textContent = text;
} else {
messageElement.innerHTML = text.replace(/\n/g, '<br>');
}
messagesContainer.appendChild(messageElement);
messagesContainer.scrollTop = messagesContainer.scrollHeight;
return messageElement;
}
/** Shows/hides loading indicator for API calls */
let apiLoadingElement = null;
function showApiLoading(show) {
if (show) {
if (!apiLoadingElement) { apiLoadingElement = displayMessage('loading', '...', 'loading'); }
} else {
if (apiLoadingElement) { apiLoadingElement.remove(); apiLoadingElement = null; }
}
}
/** Updates file processing status message in the settings modal */
function updateFileProcessingStatus(message, isError = false) {
fileProcessingStatusDiv.textContent = message;
fileProcessingStatusDiv.style.color = isError ? '#dc3545' : '#007bff';
}
/** Opens the settings modal */
function openSettingsModal() {
modelSelect.value = currentModel;
systemMessageInput.value = currentSystemMessage;
fileInput.value = ''; // Clear file input
updateFileProcessingStatus(''); // Clear status
settingsSaveButton.disabled = false;
isFileProcessing = false; // Reset flag
settingsModal.style.display = 'flex';
}
/** Closes the settings modal */
function closeSettingsModal() {
settingsModal.style.display = 'none';
updateFileProcessingStatus('');
}
/** Adds extracted text to conversation history and displays confirmation */
function addExtractedTextToConversation(fileName, text) {
if (text && text.trim().length > 0) {
const CONTEXT_MESSAGE_LIMIT = 15000; // Arbitrary limit for context message size - adjust as needed
let truncatedText = text.trim();
let wasTruncated = false;
// Simple truncation if text exceeds the limit
if (truncatedText.length > CONTEXT_MESSAGE_LIMIT) {
truncatedText = truncatedText.substring(0, CONTEXT_MESSAGE_LIMIT) + "... (truncated due to length)";
wasTruncated = true;
console.warn(Text from ${fileName} was truncated to ${CONTEXT_MESSAGE_LIMIT} characters.);
}
const contextMessage = --- Start of text extracted from ${fileName} ---\n${truncatedText}\n--- End of text extracted from ${fileName} ---;
conversationHistory.push({ role: "user", content: contextMessage });
let confirmationMsg = Successfully extracted text from ${fileName}. It has been added to the chat context.;
if (wasTruncated) {
confirmationMsg += " (Text was truncated due to length).";
}
displayMessage('system-info', confirmationMsg);
} else {
displayMessage('system-info', File ${fileName} processed, but no text content was extracted., 'error');
updateFileProcessingStatus('Processing complete, no text found.', true);
}
}
/** Processes the selected file based on its type */
async function processSelectedFile(file) {
if (!file || isFileProcessing) return;
const fileName = file.name;
const fileExtension = fileName.split('.').pop().toLowerCase();
isFileProcessing = true;
settingsSaveButton.disabled = true;
updateFileProcessingStatus(Processing ${fileName}...);
displayMessage('system-info', Starting text extraction for: ${fileName}... (This may take a while));
try {
const reader = new FileReader();
// --- Promise wrapper for FileReader ---
const readFileAs = (method) => new Promise((resolve, reject) => {
reader.onload = () => resolve(reader.result);
reader.onerror = (error) => reject(error);
reader[method](file); // Call the appropriate read method (readAsText, readAsArrayBuffer)
});
// ------------------------------------
let extractedText = "";
switch (fileExtension) {
case 'txt':
if (typeof FileReader === "undefined") throw new Error("FileReader API not supported by this browser.");
updateFileProcessingStatus('Reading text file...');
extractedText = await readFileAs('readAsText');
break;
case 'docx':
if (typeof mammoth === "undefined") throw new Error("Mammoth.js library (for DOCX) not loaded.");
updateFileProcessingStatus('Reading DOCX file (this may take a moment)...');
const arrayBufferDocx = await readFileAs('readAsArrayBuffer');
updateFileProcessingStatus('Extracting text from DOCX...');
const resultDocx = await mammoth.extractRawText({ arrayBuffer: arrayBufferDocx });
extractedText = resultDocx.value;
break;
case 'xlsx':
if (typeof XLSX === "undefined") throw new Error("SheetJS (XLSX) library not loaded.");
updateFileProcessingStatus('Reading XLSX file...');
const arrayBufferXlsx = await readFileAs('readAsArrayBuffer');
updateFileProcessingStatus('Parsing spreadsheet data...');
const workbook = XLSX.read(arrayBufferXlsx, { type: 'array' });
extractedText = "";
workbook.SheetNames.forEach((sheetName, index) => {
updateFileProcessingStatus(Extracting text from sheet: ${sheetName}...);
const worksheet = workbook.Sheets[sheetName];
const sheetText = XLSX.utils.sheet_to_txt(worksheet, { /* options */ });
if (sheetText && sheetText.trim().length > 0) {
extractedText += --- Sheet: ${sheetName} ---\n${sheetText.trim()}\n\n;
}
});
extractedText = extractedText.trim(); // Remove trailing newlines
break;
case 'pdf':
if (typeof pdfjsLib === "undefined") throw new Error("PDF.js library not loaded.");
updateFileProcessingStatus('Reading PDF file...');
const arrayBufferPdf = await readFileAs('readAsArrayBuffer');
updateFileProcessingStatus('Loading PDF document...');
const loadingTask = pdfjsLib.getDocument({ data: arrayBufferPdf });
const pdf = await loadingTask.promise;
updateFileProcessingStatus(PDF loaded (${pdf.numPages} pages). Extracting text...);
let pdfText = "";
for (let i = 1; i <= pdf.numPages; i++) {
updateFileProcessingStatus(Extracting text from PDF page ${i}/${pdf.numPages}...);
const page = await pdf.getPage(i);
const textContent = await page.getTextContent();
// Simple text concatenation - might lose some formatting/spacing context
textContent.items.forEach(item => {
pdfText += item.str + (item.hasEOL ? '\n' : ' '); // Add space or newline approximation
});
page.cleanup(); // Release page resources
pdfText += '\n\n--- Page Break ---\n\n'; // Add page separators
}
extractedText = pdfText.trim();
break;
case 'doc':
case 'xls':
throw new Error(Parsing older formats (.${fileExtension}) is not reliably supported in the browser.);
default:
throw new Error(Unsupported file type: .${fileExtension});
}
updateFileProcessingStatus('Processing Complete!');
addExtractedTextToConversation(fileName, extractedText);
} catch (error) {
console.error(Error processing file ${fileName}:, error);
updateFileProcessingStatus(Error: ${error.message || 'Unknown processing error'}, true);
displayMessage('system-info', Failed to process file ${fileName}. Error: ${error.message}, 'error');
} finally {
isFileProcessing = false;
// Re-enable save button only if modal is still open
if (settingsModal.style.display === 'flex') {
settingsSaveButton.disabled = false;
// Optionally clear status after a delay
setTimeout(() => {
if (settingsModal.style.display === 'flex') updateFileProcessingStatus('');
}, 5000);
}
}
}
/** Handles saving settings AND triggers file processing if selected */
async function handleSaveSettings() {
// 1. Save Model and System Message
currentModel = modelSelect.value;
currentSystemMessage = systemMessageInput.value.trim();
console.log("Settings updated: Model =", currentModel, "| System Message =", currentSystemMessage || "(none)");
// 2. Check for file and trigger processing (asynchronously)
const selectedFile = fileInput.files[0];
if (selectedFile && !isFileProcessing) { // Ensure not already processing
await processSelectedFile(selectedFile); // Wait for processing
} else if (!selectedFile) {
updateFileProcessingStatus(''); // Clear status if no file was selected
}
// 3. Provide feedback and close modal (if processing isn't active)
if (!isFileProcessing) {
let feedback = Model set to: ${currentModel}.;
if (currentSystemMessage) feedback += ` System message updated.`;
if (selectedFile) feedback += ` File processing started/completed for ${selectedFile.name}.`;
else feedback += ` No file selected.`
alert(feedback);
closeSettingsModal();
} else {
alert(Model and System Message saved. File processing is running for ${selectedFile.name}. Modal will remain open.);
// Don't close modal, let processSelectedFile handle UI updates.
}
}
/** Sends message to OpenAI API */
async function handleSendMessage() {
const userMessage = inputField.value.trim();
if (!userMessage) return;
if (!OPENAI_API_KEY || OPENAI_API_KEY === "YOUR_API_KEY_HERE" || !OPENAI_API_KEY.startsWith("sk-")) {
displayMessage('assistant', "API Key is missing or invalid in the code.", 'error');
return;
}
inputField.disabled = true;
sendButton.disabled = true;
displayMessage('user', userMessage);
conversationHistory.push({ role: "user", content: userMessage });
inputField.value = '';
showApiLoading(true);
const messagesToSend = [];
if (currentSystemMessage) {
messagesToSend.push({ role: "system", content: currentSystemMessage });
}
messagesToSend.push(...conversationHistory); // Includes user chat & extracted file text
// --- Context Length Check (VERY Basic) ---
const estimatedTokens = JSON.stringify(messagesToSend).length / 3; // Rough estimate
const TOKEN_LIMIT_WARNING = 30000; // Set lower than actual model limit (e.g. gpt-4o-mini 128k)
if (estimatedTokens > TOKEN_LIMIT_WARNING) {
console.warn(Estimated token count (${estimatedTokens.toFixed(0)}) is high. May exceed API limits.);
displayMessage('system-info', 'Warning: Conversation history + file content is long and might exceed API limits, potentially causing errors or dropped context.');
}
// -----------------------------------------
console.log("Sending messages to API:", messagesToSend); // Log for debugging
try {
const requestData = {
model: currentModel,
messages: messagesToSend,
};
const response = await fetch(OPENAI_API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': Bearer ${OPENAI_API_KEY} // INSECURE
},
body: JSON.stringify(requestData),
});
showApiLoading(false);
if (!response.ok) {
let errorMsg = API Error (${response.status});
try {
const errorData = await response.json();
if (errorData.error && errorData.error.message) {
errorMsg = errorData.error.message;
if (errorMsg.includes('context_length_exceeded')) {
errorMsg += "\n(Try asking shorter questions, using smaller files, or clear the chat history [feature not implemented]).";
}
} else { errorMsg = ${errorMsg} - ${response.statusText}; }
} catch (e) { errorMsg = ${errorMsg} - ${response.statusText}; }
throw new Error(errorMsg);
}
const result = await response.json();
if (result.choices && result.choices.length > 0 && result.choices[0].message) {
const assistantReply = result.choices[0].message.content;
const assistantRole = result.choices[0].message.role || 'assistant';
displayMessage(assistantRole, assistantReply);
conversationHistory.push({ role: assistantRole, content: assistantReply });
} else {
throw new Error('Received an unexpected response format from the API.');
}
} catch (error) {
showApiLoading(false);
console.error('Error sending message:', error);
if (error.message && error.message.includes('context_length_exceeded')) {
displayMessage('assistant', error.message, 'error');
} else if (error instanceof TypeError) {
displayMessage('assistant', 'Network Error: Could not connect to API. Often CORS related. Check browser console (F12).', 'error');
} else {
displayMessage('assistant', error.message || 'An unknown error occurred.', 'error');
}
} finally {
inputField.disabled = false;
sendButton.disabled = false;
inputField.focus();
}
}
// --- Event Listeners ---
sendButton.addEventListener('click', handleSendMessage);
inputField.addEventListener('keypress', (event) => {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
handleSendMessage();
}
});
settingsOpenButton.addEventListener('click', openSettingsModal);
settingsCloseButton.addEventListener('click', closeSettingsModal);
settingsSaveButton.addEventListener('click', handleSaveSettings);
settingsModal.addEventListener('click', (event) => {
if (event.target === settingsModal && !isFileProcessing) { // Don't close if processing
closeSettingsModal();
}
});
fileInput.addEventListener('change', () => {
if (fileInput.files && fileInput.files.length > 0) {
updateFileProcessingStatus(Selected: ${fileInput.files[0].name});
} else {
updateFileProcessingStatus('');
}
});
// --- Initial Setup ---
inputField.focus();
console.warn("-----------------------------------------------------");
console.warn("EXTREME SECURITY WARNING: API Key hardcoded & public!");
console.warn("DO NOT use this on a live site. For isolated testing ONLY.");
console.warn("Client-side parsing enabled for TXT, DOCX, XLSX, PDF.");
console.warn("Parsing can be SLOW and resource-intensive. Accuracy varies (esp. PDF).");
console.warn("Large file content + chat history may exceed API context limits.");
console.warn("Direct API calls may fail due to CORS.");
console.warn("-----------------------------------------------------");
</script>
</body>
</html>