BOUDI.CA Your Secure AI Analyst

Disconnected

Your chats aren’t used to improve our models. Boudica & Gemini can make mistakes, so please check the response.

The creation of svg images is still experimental and may fail. If it does, please retry with the same or modified prompt.

Using Custom Forms
Custom forms can be loaded by uploading a specific JSON file using the '+' button. Once loaded, the form will appear in the "Custom Forms" section of the sidebar. Clicking the form name will display it in the chat window. You can fill it out to add specific, structured context to your next prompt or to trigger a predefined action.

How to use Boudica Secure Chat Bot

  • Uploading Files for Analysis
    Boudica can analyze the content of various files that you upload. To do this, click the '+' button in the prompt area and select one or more files from your computer. Supported file types include: pdf, doc, docx, xls, xlsx, txt, ppt, pptx. After the file is uploaded, you can ask questions or give instructions about its content. For example, "Summarize the key points from this document," or "Create a chart showing the sales figures from this spreadsheet."

    After uploading the files the questions regarding them can be asked

  • A chatbot prompt is the message or input that a user sends to a chatbot to initiate or continue a conversation. It’s the instruction or question the chatbot is meant to respond to.

    Prompts can be:

    The clarity and context of the prompt heavily influence the quality of the chatbot’s response — especially when using AI-based bots.

    Example Prompt:

    "Show me all outstanding invoices for Q2 sorted by client."

    This tells the Boudica chatbot:

    Example Prompt - Keeping all numerical data unchanged:

    "Analyze historical transaction data, focusing on time of day (in 15-minute intervals), day of the week, and seasonal trends, to predict transaction volume. Do not change numbers"

    This tells the Boudica chatbot:


    JSON File Format Examples

    When uploading JSON files for features like External Sources or Custom Forms, they must follow a specific format:

    External Connection (e.g., cloudsql.json)

    ${escapeHTML(`{
      "kind": "sql#instance",
      "name": "your-cloudsql-instance",
      "connectionType": "CLOUD_SQL",
      "ipAddresses": [
        { "type": "PRIMARY", "ipAddress": "34.123.45.67" }
      ],
      "serverCaCert": { "cert": "-----BEGIN CERTIFICATE-----\\n...\\n-----END CERTIFICATE-----\\n" }
    }`)}

    Custom Form

    ${escapeHTML(`{
      "title": "Customer Feedback Form",
      "inputs": [
        { "type": "text", "name": "customer_name", "label": "Customer Name", "required": true },
        { "type": "textarea", "name": "feedback_text", "label": "Feedback Details" },
        { "type": "submit", "value": "Submit Feedback" }
      ],
      "method": "BOUDICA",
      "prompt": "Summarize feedback from {customer_name}: {feedback_text}"
    }`)}

    Document Redaction Rules

    ${escapeHTML(`{
      "Name": "PHI Redaction",
      "redactions": [
        { "Name": "Email Addresses", "Type": "Regex", "Pattern": "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\\\.[a-zA-Z]{2,}" }
      ]
    }`)}
    `; const helpMessage = addMessage(helpContent, 'boudica'); helpMessage.classList.add('chart-message'); // Use wide message style for better formatting promptInput.focus(); return; } // Handle "show walkthrough" command const walkthroughRegex = /^\s*show\s+walkthrough\s*$/i; if (walkthroughRegex.test(prompt)) { addMessage(`

    ${escapeHTML(prompt)}

    `, 'user'); promptInput.value = ''; adjustTextareaHeight(); // Use a small timeout to ensure the message is rendered before the tour starts, // preventing potential rendering conflicts. setTimeout(startWalkthrough, 100); promptInput.focus(); return; } // Handle "export data" command const exportDataRegex = /^\s*export\s+data\s*$/i; if (exportDataRegex.test(prompt)) { addMessage(`

    ${escapeHTML(prompt)}

    `, 'user'); promptInput.value = ''; adjustTextareaHeight(); if (!pgbc.IsActive()) { addMessage("You must be logged in to export session data.", 'system'); } else { pgbc.ExportData('pgbc-session.json'); addMessage("Your session data has been exported as 'pgbc-session.json'. You can import this file in another browser window using the 'Import' button in the sidebar settings.", 'system'); } promptInput.focus(); return; } // Handle special "logout" command const logoutRegex = /\b(logout|log out|disconnect)\b/i; if (logoutRegex.test(prompt)) { addMessage(`

    ${escapeHTML(prompt)}

    `, 'user'); promptInput.value = ''; adjustTextareaHeight(); // This function handles clearing credentials and resetting the UI handleDisconnect('You have been logged out.'); promptInput.focus(); return; } // Handle debug mode commands const debugOnRegex = /\bturn\s+(?:on\s+debug|debug\s+on)\b/i; const debugOffRegex = /\bturn\s+(?:off\s+debug|debug\s+off)\b/i; if (debugOnRegex.test(prompt)) { addMessage(`

    ${escapeHTML(prompt)}

    `, 'user'); promptInput.value = ''; adjustTextareaHeight(); pgbc.LocalStore().setItem('debugMode', 'true'); updateDebugNotice(); addMessage("Debug mode has been turned ON. Console logging is now enabled for certain operations.", 'system'); promptInput.focus(); return; } if (debugOffRegex.test(prompt)) { addMessage(`

    ${escapeHTML(prompt)}

    `, 'user'); promptInput.value = ''; adjustTextareaHeight(); pgbc.LocalStore().setItem('debugMode', 'false'); updateDebugNotice(); addMessage("Debug mode has been turned OFF.", 'system'); promptInput.focus(); return; } // Handle special "clear chat history" and "clear chat" commands const clearHistoryRegex = /\b(clear|delete)\s+(chat\s+)?history\b/i; const clearChatRegex = /^\s*clear\s+chat\s*$/i; if (clearHistoryRegex.test(prompt) || clearChatRegex.test(prompt)) { addMessage(`

    ${escapeHTML(prompt)}

    `, 'user'); promptInput.value = ''; adjustTextareaHeight(); chatHistory.innerHTML = ''; // Clear UI promptHistory = []; // Clear in-memory history if (saveSessionToggle.checked && pgbc.IsActive()) { const user = pgbc.LocalStore().getItem('username'); if (user) { pgbc.LocalStore().removeItem(`promptHistory_${user}`); // Clear local storage } } renderPromptHistory(); // Update sidebar addMessage("Chat history has been cleared.", 'system'); promptInput.focus(); return; } // Handle "close shell" command const closeShellRegex = /^\s*close\s+shell\s*$/i; if (closeShellRegex.test(prompt)) { addMessage(`

    ${escapeHTML(prompt)}

    `, 'user'); promptInput.value = ''; adjustTextareaHeight(); removeAdminForms(); // This function finds and removes the shell window addMessage("Shell closed.", 'system'); promptInput.focus(); return; } // Handle special CSV download commands const csvCommandRegex = /(download|save|export).*\bcsv\b/i; if (csvCommandRegex.test(prompt)) { addMessage(`

    ${escapeHTML(prompt)}

    `, 'user'); promptInput.value = ''; adjustTextareaHeight(); handleCsvDownload(prompt); promptInput.disabled = false; sendButton.disabled = false; if (micButton) micButton.disabled = false; cancelButton.style.display = 'none'; // Ensure cancel button is hidden promptInput.focus(); return; } // Handle special architecture overview command const architectureCommandRegex = /^\s*show\s+(me\s+the\s+)?(architecture|architectural\s+diagram|system\s+flow|system\s+overview)\s*$/i; const howItWorksRegex = /how .* work/i; if (!isImagePrompt && (architectureCommandRegex.test(prompt) || howItWorksRegex.test(prompt))) { addMessage(`

    ${escapeHTML(prompt)}

    `, 'user'); promptInput.value = ''; adjustTextareaHeight(); const responseContent = `

    Architectural Overview

    Of course. Here is a diagram showing the general data flow for a user prompt:

    Boudica Architecture Flow

    Would you like to download this image? Click here to download.

    `; const imageMessage = addMessage(responseContent, 'boudica'); imageMessage.classList.add('chart-message'); // Re-use styles for wide content promptInput.focus(); return; } // Safeguard: Should not be able to submit if not connected. if (!pgbc.IsActive()) { showLoginModal(); return; } promptInput.value = ''; adjustTextareaHeight(); // Reset height after submit // Add or update prompt in history for context and UI, but only for user-typed prompts if (!isHiddenPrompt) { const existingHistoryIndex = promptHistory.findIndex(h => h.prompt === prompt); if (existingHistoryIndex > -1) { // If prompt exists, move it to the top const [item] = promptHistory.splice(existingHistoryIndex, 1); promptHistory.unshift(item); } else { // Otherwise, add it as a new item promptHistory.unshift({ prompt: prompt, sql: null }); } if (promptHistory.length > MAX_HISTORY) promptHistory.pop(); renderPromptHistory(); } promptInput.disabled = true; sendButton.disabled = true; if (micButton) micButton.disabled = true; cancelButton.style.display = 'block'; // Show cancel button // Add an initial, conversational acknowledgement message. const acknowledgementMessages = [ "Of course, I'll be happy to work on that for you to get the information you've asked for.", "Certainly. I'm processing your request now.", "I see you're asking about that. I'll start getting the information for you now.", "Right away! I'm looking into that for you.", "I will go and see what is available for you.", "Understood. I'm now gathering the information you requested." ]; const acknowledgement = acknowledgementMessages[Math.floor(Math.random() * acknowledgementMessages.length)]; const thinkingMessage = addMessage(`

    ${acknowledgement}

    `, 'boudica'); // Wait for a moment so the user can read the acknowledgement before showing the "thinking" animation. await new Promise(resolve => setTimeout(resolve, 2500)); const randomMessage = getRandomThinkingMessage(); if (isImagePrompt) { // Transform the thinkingMessage into the image placeholder thinkingMessage.innerHTML = `

    Generating Image...

    `; // Text inside the placeholder thinkingMessage.classList.remove('thinking'); thinkingMessage.classList.add('image-placeholder'); } else if (isDocumentPrompt) { // Transform the thinkingMessage into the document placeholder thinkingMessage.innerHTML = `

    Creating Document...

    `; thinkingMessage.classList.remove('thinking'); thinkingMessage.classList.add('document-placeholder'); thinkingMessage.style.animation = 'pulsate-document-bg 2.5s infinite ease-in-out'; } else { // Now, update the message bubble to show the "thinking" state. thinkingMessage.innerHTML = `
    ${randomMessage}
    `; } try { currentAbortController = new AbortController(); // New controller for Boudica call const boudica = pgbc.Boudica(); const boudicaResponse = await boudica.CallBoudica(actualPrompt, historyForBoudica, currentAbortController.signal); // If it's not a client command, destructure the response for normal processing. const { data, error, sql, image_data, html_data, job_id, message } = boudicaResponse; // NEW: Check if a background job was started if (job_id) { // The 'thinking' message is still on screen. Update it with the job info. thinkingMessage.innerHTML = `

    ${escapeHTML(message || `Background job started with ID: ${job_id}`)}

    I will check on its status periodically and notify you upon completion.

    `; thinkingMessage.classList.remove('thinking'); // Start polling for the job status. pollJobStatus(job_id); } else { // This is the existing logic for handling synchronous responses. // After getting the response, update the history item with the SQL and save it, if it's not a hidden prompt if (!isHiddenPrompt) { if (sql) { const currentHistoryItem = promptHistory.find(h => h.prompt === prompt); if (currentHistoryItem) { currentHistoryItem.sql = sql; } } if (saveSessionToggle.checked && pgbc.IsActive()) { const user = pgbc.LocalStore().getItem('username'); if (user) { pgbc.LocalStore().setItem(`promptHistory_${user}`, JSON.stringify(promptHistory)); } } } let responseContent = ''; const isDebugMode = pgbc.LocalStore().getItem('debugMode') === 'true'; if (image_data) { // Case 0: We have an SVG image to display. const imageId = `svg-image-${Date.now()}`; responseContent = `

    Here is the image you requested:

    ${image_data}
    `; thinkingMessage.innerHTML = responseContent; thinkingMessage.classList.remove('thinking', 'image-placeholder'); thinkingMessage.classList.remove('document-placeholder'); thinkingMessage.classList.add('chart-message'); // Ensure it has final message styles } else if (html_data) { // NEW: Case for HTML data const iframeId = `html-iframe-${Date.now()}`; // The content for srcdoc must have its double-quotes escaped to be a valid attribute value. const escapedSrcDoc = html_data.replace(/"/g, '"'); responseContent = `

    Here is the content you requested:

    `; thinkingMessage.innerHTML = responseContent; thinkingMessage.classList.remove('thinking'); thinkingMessage.classList.remove('image-placeholder'); thinkingMessage.classList.remove('document-placeholder'); thinkingMessage.classList.add('chart-message'); } else { // Not an image response if (isImagePrompt || isDocumentPrompt) { // If it was a special prompt but no special data came back (e.g., error), remove placeholder classes thinkingMessage.classList.remove('document-placeholder', 'image-placeholder'); thinkingMessage.classList.add('boudica-message'); // Re-add base message class } // Case 1: Success with tabular data. Explicitly check if it's an array. if (data && Array.isArray(data) && data.length > 0) { responseContent += `

    Here is the information you requested.${sql && isDebugMode && !isHiddenPrompt ? " I've also included the SQL query I used to find it." : ""}

    `; if (sql && isDebugMode && !isHiddenPrompt) { responseContent += `
    ${escapeHTML(sql)}
    `; } responseContent += `

    Data

    `; Object.keys(data[0]).forEach(key => { responseContent += ``; }); responseContent += ''; data.forEach(row => { responseContent += ''; Object.values(row).forEach(value => { responseContent += ``; }); responseContent += ''; }); responseContent += '
    ${escapeHTML(key)}
    ${escapeHTML(String(value))}
    '; } else if (typeof data === 'string' && data.length > 0) { // Case 1b: Success, but the response is a string message, not tabular data. responseContent = `

    ${escapeHTML(data)}

    `; } else if (sql && !error) { // Case 2: Success, but no data returned from query // Case 2: Success, but no data returned from query if (isDebugMode && !isHiddenPrompt) { responseContent += `

    I ran the query successfully, but it didn't return any results. Here is the SQL I used:

    `; responseContent += `
    ${escapeHTML(sql)}
    `; } else { responseContent += `

    I ran a query successfully, but it didn't return any results.

    `; } } else if (error) { // Case 3: An error occurred. if (isDebugMode) { // Subcase 3a: Debug mode is ON. Show detailed error and SQL. responseContent += `

    It seems that the system was unable to run the query. I have placed it here for you to check:

    `; if (sql && !isHiddenPrompt) { responseContent += `
    ${escapeHTML(sql)}
    `; } responseContent += `

    Error details: ${escapeHTML(error)}

    `; } else { // Subcase 3b: Debug mode is OFF. Show a generic, user-friendly message. responseContent = "

    I am afraid that I have been unable to fulfil your request. Please retry. You can do this with the same prompt, or a slightly modified one.

    "; } } else if (typeof actualPrompt === 'string' && /^\s*(run\s+shell\s+)?insert/i.test(actualPrompt)) { // Case 4: The query was likely a successful INSERT from a form, which returns no data. responseContent = "

    I am updating the information for you.

    "; } else { // Case 5: Truly empty/unexpected response. responseContent = "

    Sorry, I cannot find that information or understand the request. Could you please try rephrasing it?

    "; } thinkingMessage.innerHTML = responseContent; // Update the message content thinkingMessage.classList.remove('thinking'); thinkingMessage.classList.add('chart-message'); // Use wide message style for tables if (sql && isDebugMode && !isHiddenPrompt) Prism.highlightAll(); // Trigger syntax highlighting // If we have tabular data, save it as a CSV for potential download. if (data && Array.isArray(data) && data.length > 0) { const user = pgbc.LocalStore().getItem('username'); if (user) { // --- Store data as CSV in localStorage --- const CSV_HISTORY_KEY = `csvExportHistory_${user}`; const CSV_KEY_PREFIX = 'csv_export_'; const MAX_CSV_HISTORY = 5; const csvData = convertToCSV(data); const csvKey = `${CSV_KEY_PREFIX}${Date.now()}`; let csvHistory = JSON.parse(pgbc.LocalStore().getItem(CSV_HISTORY_KEY) || '[]'); // Add new item metadata to history csvHistory.unshift({ key: csvKey, name: prompt }); // Store the actual CSV data pgbc.LocalStore().setItem(csvKey, csvData); // If history is too long, remove the oldest item if (csvHistory.length > MAX_CSV_HISTORY) { const oldestEntry = csvHistory.pop(); if (oldestEntry && oldestEntry.key) { pgbc.LocalStore().removeItem(oldestEntry.key); } } // Save the updated history array back to local storage pgbc.LocalStore().setItem(CSV_HISTORY_KEY, JSON.stringify(csvHistory)); // After saving, re-render the CSV history list in the sidebar renderCsvHistory(); } } // If graph toggle is on and we have data, render a chart using Chart.js if (data && Array.isArray(data) && data.length > 0 && graphToggle.checked) { const chartMessageContainer = addMessage('', 'boudica'); chartMessageContainer.classList.add('chart-message'); try { renderChartWithChartJS(data, chartMessageContainer, prompt, requestedChartType); } catch (chartError) { chartMessageContainer.remove(); addMessage(`Chart Generation: ${escapeHTML(chartError.message)}`, 'system'); console.error("Chart generation error:", chartError); } } } } } catch (err) { thinkingMessage.remove(); // Remove the "thinking" message on any error or cancellation if (err.name === 'AbortError') { // If aborted, the thinkingMessage might still be the placeholder. addMessage("Request cancelled.", 'system'); // Now add the confirmation } else { let errorMessage = err.message; if (errorMessage.includes('Failed to fetch')) { errorMessage = 'I was unable to find any data. Please modify your prompt and retry'; } addMessage(`Error: ${escapeHTML(errorMessage)}`, 'system'); console.error("Boudica call error:", err); } } finally { if (userMessageElement) { userMessageElement.classList.remove('waiting-for-response'); } promptInput.disabled = false; sendButton.disabled = false; if (micButton) micButton.disabled = false; currentAbortController = null; // Clear the controller promptInput.focus(); } }); // --- Session Import/Export Button Handlers --- if (exportSessionLink) { exportSessionLink.addEventListener('click', (e) => { e.preventDefault(); if (!pgbc.IsActive()) { addMessage("You must be logged in to export session data.", 'system'); return; } pgbc.ExportData('pgbc-session.json'); addMessage("Your session data has been exported as 'pgbc-session.json'. You can import this file in another browser window using the 'Import' button.", 'system'); }); } if (importSessionLink) { importSessionLink.addEventListener('click', (e) => { e.preventDefault(); importSessionFile.click(); }); } if (importSessionFile) { importSessionFile.addEventListener('change', async (event) => { const file = event.target.files[0]; if (!file) return; try { await pgbc.ImportData(file); alert('Import successful! The page will now reload to apply the new settings.'); window.location.reload(); } catch (error) { console.error("Import failed:", error); alert(`Import failed: ${error.message}`); } // Reset the file input so the 'change' event fires again for the same file importSessionFile.value = ''; }); } });