lokiai / Rulebased-chatbot.html
ParthSadaria's picture
Rename SPOILER_v1.html to Rulebased-chatbot.html
5ff78b1 verified
raw
history blame
47.2 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Office Procedure Expert System</title>
<style>
:root {
--primary: #1a3a6b;
--secondary: #3498db;
--accent: #f39c12;
--background: #f5f7fa;
--text: #2c3e50;
--light: #ecf0f1;
--success: #2ecc71;
--danger: #e74c3c;
--shadow-light: rgba(0, 0, 0, 0.08);
--shadow-medium: rgba(0, 0, 0, 0.12);
--border-radius-small: 4px;
--border-radius-medium: 8px;
--border-radius-large: 1rem;
--border-radius-round: 50%;
--transition-speed: 0.3s;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
body {
background-color: var(--background);
color: var(--text);
display: flex;
flex-direction: column;
min-height: 100vh;
transition: background-color var(--transition-speed) ease; /* Smooth transition for potential theme changes */
}
/* Drag over effect */
body.drag-active::before {
content: 'Drop JSON file to import rules';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(52, 152, 219, 0.8);
color: white;
display: flex;
justify-content: center;
align-items: center;
font-size: 1.5rem;
font-weight: bold;
z-index: 1000;
pointer-events: none; /* Important */
opacity: 0;
animation: fadeIn 0.3s ease forwards;
}
header {
background-color: var(--primary);
color: white;
padding: 1rem;
text-align: center;
box-shadow: 0 2px 5px var(--shadow-medium);
}
h1 {
margin-bottom: 0.5rem;
}
main {
display: flex;
flex-direction: column;
flex: 1;
padding: 1.5rem; /* Slightly increased padding */
max-width: 1300px; /* Slightly wider max-width */
margin: 1rem auto; /* Add margin top/bottom */
width: 100%;
gap: 1.5rem; /* Increased gap */
}
@media (min-width: 768px) {
main {
flex-direction: row;
}
}
/* Card Styles */
.card {
background: white;
border-radius: var(--border-radius-medium);
box-shadow: 0 3px 10px var(--shadow-light);
overflow: hidden;
display: flex;
flex-direction: column;
transition: box-shadow var(--transition-speed) ease;
}
.card:hover {
box-shadow: 0 6px 15px var(--shadow-medium); /* Subtle lift on hover */
}
.chat-container {
flex: 2;
margin-bottom: 1rem; /* Keep margin for mobile */
height: calc(100vh - 150px); /* Limit height on desktop */
max-height: 800px; /* Max height */
}
.rules-container {
flex: 1;
padding: 1.5rem;
max-height: calc(100vh - 150px); /* Match chat height */
overflow: hidden; /* Ensure padding is respected */
margin-bottom: 1rem; /* Keep margin for mobile */
}
@media (min-width: 768px) {
.chat-container, .rules-container {
margin-bottom: 0; /* Remove bottom margin on desktop */
}
}
.rules-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
padding-bottom: 0.8rem;
border-bottom: 1px solid var(--light);
}
.chat-header {
background-color: var(--primary);
color: white;
padding: 1rem 1.5rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.chat-header h2 { font-size: 1.1rem; }
.chat-messages {
flex: 1;
padding: 1.5rem;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 1rem;
/* Custom Scrollbar */
scrollbar-width: thin;
scrollbar-color: var(--secondary) var(--light);
}
.chat-messages::-webkit-scrollbar { width: 8px; }
.chat-messages::-webkit-scrollbar-track { background: var(--light); border-radius: 4px; }
.chat-messages::-webkit-scrollbar-thumb { background-color: var(--secondary); border-radius: 4px; }
.message {
max-width: 80%;
padding: 0.8rem 1.2rem;
border-radius: var(--border-radius-large);
margin-bottom: 0.5rem;
box-shadow: 0 1px 3px var(--shadow-light);
position: relative;
word-wrap: break-word;
opacity: 0; /* Start hidden for animation */
transform: translateY(10px); /* Start slightly lower */
animation: messageFadeIn 0.5s ease forwards;
}
@keyframes messageFadeIn {
to {
opacity: 1;
transform: translateY(0);
}
}
.user-message {
background-color: var(--secondary);
color: white;
align-self: flex-end;
border-bottom-right-radius: var(--border-radius-small);
}
.bot-message {
background-color: var(--light);
color: var(--text);
align-self: flex-start;
border-bottom-left-radius: var(--border-radius-small);
}
.chat-input {
display: flex;
padding: 1rem 1.5rem;
border-top: 1px solid var(--light);
background-color: white;
gap: 0.5rem; /* Add gap */
}
.chat-input input {
flex: 1;
padding: 0.8rem 1.2rem;
border: 1px solid var(--light);
border-radius: var(--border-radius-round); /* Fully rounded */
outline: none;
transition: border-color var(--transition-speed) ease, box-shadow var(--transition-speed) ease;
}
.chat-input input:focus {
border-color: var(--secondary);
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.2); /* Focus ring */
}
/* Button Base Styles */
button {
padding: 0.7rem 1.2rem; /* Adjusted padding */
background-color: var(--secondary);
color: white;
border: none;
border-radius: var(--border-radius-small);
cursor: pointer;
transition: all var(--transition-speed) ease; /* Smooth transitions */
display: inline-flex; /* Align icon and text */
align-items: center;
justify-content: center;
gap: 0.5rem;
font-weight: 500; /* Slightly bolder */
white-space: nowrap; /* Prevent wrapping */
}
button:hover:not(:disabled) {
background-color: var(--primary);
filter: brightness(1.1); /* Brighter on hover */
transform: translateY(-2px); /* Lift effect */
box-shadow: 0 4px 8px var(--shadow-light);
}
button:active:not(:disabled) {
transform: translateY(0); /* Press down effect */
filter: brightness(0.9);
box-shadow: none;
}
button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
button.danger { background-color: var(--danger); }
button.danger:hover:not(:disabled) { background-color: #c0392b; } /* Darker red */
button.success { background-color: var(--success); }
button.success:hover:not(:disabled) { background-color: #27ae60; } /* Darker green */
button.outline {
background-color: transparent;
color: var(--secondary);
border: 1px solid var(--secondary);
}
button.outline:hover:not(:disabled) {
background-color: var(--secondary);
color: white;
filter: none; /* Remove brightness filter for outline */
}
.rule-form {
display: flex;
flex-direction: column;
gap: 0.8rem; /* Increased gap */
margin-bottom: 1.5rem; /* Increased margin */
}
.rule-form textarea {
padding: 0.8rem;
border: 1px solid var(--light);
border-radius: var(--border-radius-medium); /* Match card radius */
resize: vertical;
min-height: 80px;
font-size: 0.9rem;
transition: border-color var(--transition-speed) ease, box-shadow var(--transition-speed) ease;
}
.rule-form textarea:focus {
border-color: var(--secondary);
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.2); /* Focus ring */
outline: none;
}
.rule-actions {
display: flex;
gap: 0.5rem;
}
.rules-list {
overflow-y: auto;
flex: 1;
padding-right: 0.5rem; /* Space for scrollbar */
/* Custom Scrollbar */
scrollbar-width: thin;
scrollbar-color: var(--secondary) var(--light);
}
.rules-list::-webkit-scrollbar { width: 6px; }
.rules-list::-webkit-scrollbar-track { background: var(--light); border-radius: 3px; }
.rules-list::-webkit-scrollbar-thumb { background-color: var(--secondary); border-radius: 3px; }
.rule-item {
padding: 1rem;
border: 1px solid var(--light);
border-radius: var(--border-radius-medium);
margin-bottom: 1rem; /* Increased spacing */
position: relative;
background-color: white; /* Ensure background for shadow */
box-shadow: 0 1px 3px var(--shadow-light);
transition: all var(--transition-speed) ease; /* Transition all properties */
opacity: 0; /* Start hidden for animation */
animation: ruleFadeIn 0.4s ease forwards;
}
@keyframes ruleFadeIn {
to { opacity: 1; }
}
.rule-item.deleting {
animation: ruleFadeOut 0.4s ease forwards;
}
@keyframes ruleFadeOut {
to {
opacity: 0;
transform: scale(0.95);
margin-bottom: 0;
padding-top: 0;
padding-bottom: 0;
height: 0;
border: none;
}
}
.rule-item:hover {
border-color: var(--secondary);
box-shadow: 0 4px 8px var(--shadow-medium);
transform: translateY(-2px); /* Subtle lift */
}
.rule-item p {
margin-bottom: 1rem; /* Increased spacing */
word-wrap: break-word;
line-height: 1.6; /* Improve readability */
}
.rule-actions-inline {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
}
/* Smaller buttons for inline actions */
.rule-actions-inline button {
padding: 0.4rem 0.8rem;
font-size: 0.8rem;
}
.language-selector {
padding: 0.5rem 0.8rem;
border-radius: var(--border-radius-small);
border: 1px solid var(--light);
background-color: white;
margin-left: 0.5rem;
transition: border-color var(--transition-speed) ease, box-shadow var(--transition-speed) ease;
cursor: pointer;
}
.language-selector:hover {
border-color: var(--secondary);
}
.language-selector:focus {
border-color: var(--secondary);
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.2); /* Focus ring */
outline: none;
}
.system-message {
text-align: center;
color: var(--text);
opacity: 0.7;
margin: 0.5rem 0;
font-size: 0.85rem; /* Slightly larger */
font-style: italic;
}
.loader {
display: inline-block;
width: 18px; /* Slightly smaller */
height: 18px;
border: 3px solid rgba(255, 255, 255, 0.3);
border-radius: var(--border-radius-round);
border-top-color: white;
animation: spin 1s ease-in-out infinite;
/* margin-right: 0.5rem; /* Gap handles spacing */
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.hidden {
display: none !important; /* Ensure it's hidden */
}
.visually-hidden { /* For animated hiding */
opacity: 0 !important;
transform: scale(0.95) translateY(-10px) !important;
pointer-events: none !important;
}
.settings-btn {
background: none;
border: none;
color: white;
font-size: 1.5rem; /* Larger icon */
cursor: pointer;
padding: 0.3rem; /* Add padding for easier clicking */
border-radius: var(--border-radius-round);
transition: background-color var(--transition-speed) ease, transform var(--transition-speed) ease;
}
.settings-btn:hover {
background-color: rgba(255, 255, 255, 0.2); /* Subtle background */
transform: rotate(15deg); /* Rotation effect */
filter: none;
box-shadow: none;
}
.settings-panel {
position: absolute;
top: 70px; /* Position below header */
right: 2rem; /* Align with main content padding */
background: white;
border-radius: var(--border-radius-medium);
box-shadow: 0 5px 20px var(--shadow-medium);
padding: 1.5rem;
z-index: 100;
width: 320px; /* Slightly wider */
/* Animation */
transition: opacity var(--transition-speed) ease, transform var(--transition-speed) ease;
opacity: 1;
transform: translateY(0);
}
.settings-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem; /* Increased spacing */
padding-bottom: 0.8rem;
border-bottom: 1px solid var(--light);
}
.settings-header h3 { font-size: 1.2rem; }
.close-btn {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: var(--text);
padding: 0.2rem;
line-height: 1; /* Prevent extra space */
transition: color var(--transition-speed) ease, transform var(--transition-speed) ease;
}
.close-btn:hover {
color: var(--danger);
transform: scale(1.1);
filter: none;
box-shadow: none;
}
.settings-body {
display: flex;
flex-direction: column;
gap: 1.2rem; /* Increased spacing */
}
.setting-item {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.setting-item label {
font-weight: 600; /* Slightly bolder */
display: flex; /* Align icon */
align-items: center;
}
.setting-item input {
padding: 0.7rem 1rem;
border: 1px solid var(--light);
border-radius: var(--border-radius-small);
transition: border-color var(--transition-speed) ease, box-shadow var(--transition-speed) ease;
}
.setting-item input:focus {
border-color: var(--secondary);
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.2); /* Focus ring */
outline: none;
}
.tooltip {
position: relative;
display: inline-block;
margin-left: 0.5rem;
cursor: help;
color: var(--secondary); /* Make icon color consistent */
}
.tooltip .tooltiptext {
visibility: hidden;
width: 220px; /* Wider tooltip */
background-color: var(--text);
color: white;
text-align: center;
border-radius: var(--border-radius-small);
padding: 0.6rem; /* Adjusted padding */
position: absolute;
z-index: 1;
bottom: 135%; /* Position higher */
left: 50%;
transform: translateX(-50%);
opacity: 0;
transition: opacity var(--transition-speed) ease, visibility var(--transition-speed) ease;
font-size: 0.85rem;
line-height: 1.4;
box-shadow: 0 2px 5px var(--shadow-medium);
}
.tooltip .tooltiptext::after { /* Tooltip arrow */
content: "";
position: absolute;
top: 100%;
left: 50%;
margin-left: -5px;
border-width: 5px;
border-style: solid;
border-color: var(--text) transparent transparent transparent;
}
.tooltip:hover .tooltiptext {
visibility: visible;
opacity: 1;
}
/* Add some subtle animations */
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
</style>
</head>
<body>
<header>
<h1>Office Procedure Expert System</h1>
<p>General Administration Department</p>
</header>
<main>
<!-- Added card class -->
<div class="chat-container card">
<div class="chat-header">
<div>
<h2>Office Procedure Assistant</h2>
</div>
<div class="chat-controls">
<select id="language-selector" class="language-selector" title="Select language">
<option value="en">English</option>
<option value="gu">Gujarati</option>
<option value="hi">Hindi</option>
</select>
<button id="settings-btn" class="settings-btn" title="Settings">⚙️</button>
</div>
</div>
<div class="chat-messages" id="chat-messages">
<div class="message bot-message">
Hello! I'm your Office Procedure Assistant. Ask me any questions about office procedures and rules, and I'll help you find the information you need.
</div>
<div class="system-message">
Add rules in the panel to the right or drag & drop a JSON file onto the page to import rules.
</div>
</div>
<div class="chat-input">
<input type="text" id="user-input" placeholder="Type your question here...">
<button id="send-btn" title="Send message">
<span id="loader" class="loader hidden"></span>
<span id="send-text">Send</span>
<!-- Feather icons replacement for Send icon -->
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-send"><line x1="22" y1="2" x2="11" y2="13"></line><polygon points="22 2 15 22 11 13 2 9 22 2"></polygon></svg>
</button>
</div>
</div>
<!-- Added card class -->
<div class="rules-container card">
<div class="rules-header">
<h2>Knowledge Base</h2>
<!-- Added outline class and icon -->
<button id="export-rules" class="outline" title="Export rules as JSON">
Export
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-download"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>
</button>
</div>
<div class="rule-form">
<textarea id="rule-content" placeholder="Enter a rule or procedure here..."></textarea>
<div class="rule-actions">
<!-- Added icon -->
<button id="add-rule" class="success" title="Add or update rule">
<span id="add-rule-text">Add Rule</span>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-plus-circle"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="16"></line><line x1="8" y1="12" x2="16" y2="12"></line></svg>
</button>
<!-- Added outline class and icon -->
<button id="clear-form" class="outline" title="Clear form">
Clear
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-x-circle"><circle cx="12" cy="12" r="10"></circle><line x1="15" y1="9" x2="9" y2="15"></line><line x1="9" y1="9" x2="15" y2="15"></line></svg>
</button>
</div>
</div>
<div class="rules-list" id="rules-list">
<!-- Rules will be added here dynamically -->
</div>
</div>
</main>
<!-- Added visually-hidden class for animation -->
<div id="settings-panel" class="settings-panel visually-hidden">
<div class="settings-header">
<h3>Settings</h3>
<button id="close-settings" class="close-btn" title="Close settings">×</button>
</div>
<div class="settings-body">
<div class="setting-item">
<label for="api-endpoint">
API Endpoint
<div class="tooltip">ℹ️
<span class="tooltiptext">The base URL for the AI model API (e.g., Hugging Face Space, local endpoint).</span>
</div>
</label>
<input type="text" id="api-endpoint" value="https://parthsadaria-lokiai.hf.space/chat/completions">
</div>
<div class="setting-item">
<label for="api-model">
Model
<div class="tooltip">ℹ️
<span class="tooltiptext">The specific model identifier to use (e.g., 'gemini', 'mistralai/Mistral-7B-Instruct-v0.1').</span>
</div>
</label>
<input type="text" id="api-model" value="gemini">
</div>
<div class="setting-item">
<label for="api-key">
Authorization Key
<div class="tooltip">ℹ️
<span class="tooltiptext">Your API key or token for authentication (often prefixed with 'Bearer '). Keep this secure.</span>
</div>
</label>
<input type="password" id="api-key" value="sigma">
</div>
<!-- Added icon -->
<button id="save-settings" class="success" title="Save API settings">
Save Settings
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-save"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"></path><polyline points="17 21 17 13 7 13 7 21"></polyline><polyline points="7 3 7 8 15 8"></polyline></svg>
</button>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// DOM Elements (Grouped for clarity)
const chatMessages = document.getElementById('chat-messages');
const userInput = document.getElementById('user-input');
const sendBtn = document.getElementById('send-btn');
const loader = document.getElementById('loader');
const sendText = document.getElementById('send-text');
const sendIcon = sendBtn.querySelector('svg'); // Get the send icon
const ruleContent = document.getElementById('rule-content');
const addRuleBtn = document.getElementById('add-rule');
const addRuleText = document.getElementById('add-rule-text');
const clearFormBtn = document.getElementById('clear-form');
const rulesList = document.getElementById('rules-list');
const exportRulesBtn = document.getElementById('export-rules');
const languageSelector = document.getElementById('language-selector');
const settingsBtn = document.getElementById('settings-btn');
const settingsPanel = document.getElementById('settings-panel');
const closeSettingsBtn = document.getElementById('close-settings');
const saveSettingsBtn = document.getElementById('save-settings');
const apiEndpointInput = document.getElementById('api-endpoint');
const apiModelInput = document.getElementById('api-model');
const apiKeyInput = document.getElementById('api-key');
// App State
let rules = [];
let settings = {
apiEndpoint: "https://parthsadaria-lokiai.hf.space/chat/completions",
model: "gemini",
apiKey: "sigma"
};
// --- Initialization ---
loadSettings();
loadRules();
setupDragAndDrop(); // Initialize drag & drop listeners
// --- Local Storage Functions ---
function loadRules() {
const savedRules = localStorage.getItem('chatbotRules');
if (savedRules) {
try {
rules = JSON.parse(savedRules);
} catch (e) {
console.error("Error parsing rules from localStorage:", e);
rules = []; // Reset if corrupted
localStorage.removeItem('chatbotRules'); // Clear corrupted data
}
}
renderRules();
}
function loadSettings() {
const savedSettings = localStorage.getItem('chatbotSettings');
if (savedSettings) {
try {
settings = JSON.parse(savedSettings);
} catch (e) {
console.error("Error parsing settings from localStorage:", e);
// Keep default settings if parsing fails
localStorage.removeItem('chatbotSettings'); // Clear corrupted data
}
}
// Always update input fields from the current 'settings' object
apiEndpointInput.value = settings.apiEndpoint;
apiModelInput.value = settings.model;
apiKeyInput.value = settings.apiKey;
}
function saveRules() {
localStorage.setItem('chatbotRules', JSON.stringify(rules));
}
function saveSettings() {
settings = {
apiEndpoint: apiEndpointInput.value.trim(),
model: apiModelInput.value.trim(),
apiKey: apiKeyInput.value.trim() // Trim whitespace
};
localStorage.setItem('chatbotSettings', JSON.stringify(settings));
closeSettingsPanel(); // Close panel after saving
addSystemMessage("Settings saved successfully."); // Feedback
}
// --- UI Rendering and Updates ---
function renderRules() {
rulesList.innerHTML = ''; // Clear previous list
if (rules.length === 0) {
rulesList.innerHTML = '<div class="system-message">No rules added yet. Add rules or drag & drop a JSON file.</div>';
return;
}
rules.forEach((rule, index) => {
const ruleElement = document.createElement('div');
ruleElement.className = 'rule-item';
ruleElement.dataset.index = index; // Use data attribute for index
ruleElement.innerHTML = `
<p>${escapeHtml(rule)}</p> <!-- Escape HTML to prevent XSS -->
<div class="rule-actions-inline">
<button class="edit-rule outline" title="Edit rule">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>
</button>
<button class="delete-rule danger" title="Delete rule">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg>
</button>
</div>
`;
// Add event listeners directly here for simplicity
ruleElement.querySelector('.edit-rule').addEventListener('click', handleEditRule);
ruleElement.querySelector('.delete-rule').addEventListener('click', handleDeleteRule);
rulesList.appendChild(ruleElement);
});
}
// Function to escape HTML special characters
function escapeHtml(unsafe) {
if (!unsafe) return '';
return unsafe
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
function addMessage(message, isUser = false) {
const messageElement = document.createElement('div');
messageElement.className = `message ${isUser ? 'user-message' : 'bot-message'}`;
// Use textContent to prevent XSS from user input or bot response
messageElement.textContent = message;
chatMessages.appendChild(messageElement);
// Scroll to bottom smoothly
chatMessages.scrollTo({ top: chatMessages.scrollHeight, behavior: 'smooth' });
}
function addSystemMessage(message) {
const messageElement = document.createElement('div');
messageElement.className = 'system-message';
messageElement.textContent = message;
chatMessages.appendChild(messageElement);
chatMessages.scrollTo({ top: chatMessages.scrollHeight, behavior: 'smooth' });
}
function setLoading(isLoading) {
if (isLoading) {
loader.classList.remove('hidden');
sendText.classList.add('hidden'); // Hide text
sendIcon.classList.add('hidden'); // Hide icon
sendBtn.disabled = true;
userInput.disabled = true;
} else {
loader.classList.add('hidden');
sendText.classList.remove('hidden'); // Show text
sendIcon.classList.remove('hidden'); // Show icon
sendBtn.disabled = false;
userInput.disabled = false;
userInput.focus(); // Refocus input after sending
}
}
function openSettingsPanel() {
settingsPanel.classList.remove('visually-hidden');
}
function closeSettingsPanel() {
settingsPanel.classList.add('visually-hidden');
}
// --- Core Logic Functions ---
async function sendToAI(message) {
// Basic validation for settings
if (!settings.apiEndpoint || !settings.model || !settings.apiKey) {
addSystemMessage("API settings are incomplete. Please configure them in the settings panel.");
openSettingsPanel(); // Prompt user to open settings
return "Error: API settings are not configured.";
}
try {
const currentLanguage = languageSelector.value;
let context = rules.length > 0
? `Based on the following rules:\n\n${rules.map((r, i) => `Rule ${i + 1}: ${r}`).join('\n\n')}\n\n`
: "You are a general office procedure assistant.\n\n";
const systemPrompt = `You are an AI assistant for the General Administration Department, specializing in office procedures and rules. The user is interacting in ${currentLanguage === 'en' ? 'English' : currentLanguage === 'gu' ? 'Gujarati' : 'Hindi'}. Respond ONLY in ${currentLanguage === 'en' ? 'English' : currentLanguage === 'gu' ? 'Gujarati' : 'Hindi'}. ${context}Answer the user's query concisely and accurately. If the query is outside the scope of the provided rules or general office procedures, politely state that you cannot answer. Do not invent information.`;
const payload = {
model: settings.model,
messages: [
{ role: "system", content: systemPrompt },
{ role: "user", content: message }
],
temperature: 0.5,
max_tokens: 500
};
const response = await fetch(settings.apiEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${settings.apiKey}` // Standard Bearer token format
},
body: JSON.stringify(payload)
});
if (!response.ok) {
let errorMsg = `API request failed with status ${response.status}`;
try {
const errorData = await response.json();
errorMsg += `: ${errorData.error?.message || JSON.stringify(errorData)}`;
} catch(e) { /* Ignore if response body isn't JSON */ }
throw new Error(errorMsg);
}
const data = await response.json();
// Handle different possible response structures
let botResponse = "Sorry, I couldn't get a valid response."; // Default
if (data.choices && data.choices[0] && data.choices[0].message && data.choices[0].message.content) {
botResponse = data.choices[0].message.content.trim();
} else if (data.message && data.message.content) { // Some APIs might have a different structure
botResponse = data.message.content.trim();
} else {
console.warn("Unexpected API response structure:", data);
}
return botResponse;
} catch (error) {
console.error('Error sending message to AI:', error);
// Provide more specific error feedback if possible
const userFriendlyError = error.message.includes("Failed to fetch")
? "I couldn't connect to the AI service. Please check the API Endpoint and your network connection."
: error.message.includes("401") || error.message.includes("Unauthorized")
? "Authentication failed. Please check your Authorization Key in the settings."
: error.message.includes("404")
? "The AI model or endpoint was not found. Please check the API Endpoint and Model settings."
: "I encountered an error. Please check the console for details or try again later.";
return userFriendlyError;
}
}
async function handleSendMessage() {
const message = userInput.value.trim();
if (!message) return;
addMessage(message, true);
userInput.value = '';
if (rules.length === 0 && !localStorage.getItem('rulesWarned')) {
addSystemMessage("Tip: Add specific rules in the knowledge base for more accurate answers.");
localStorage.setItem('rulesWarned', 'true'); // Show only once per session maybe? Or use sessionStorage
}
setLoading(true);
const response = await sendToAI(message);
setLoading(false);
addMessage(response);
}
function handleAddOrUpdateRule() {
const content = ruleContent.value.trim();
if (!content) return;
const editIndex = ruleContent.dataset.editIndex; // Use data attribute
if (editIndex !== undefined) {
// Update existing rule
rules[parseInt(editIndex)] = content;
delete ruleContent.dataset.editIndex; // Clean up attribute
addRuleText.textContent = 'Add Rule';
addRuleBtn.title = "Add rule";
addSystemMessage(`Rule ${parseInt(editIndex) + 1} updated.`);
} else {
// Add new rule
rules.push(content);
addSystemMessage(`Rule ${rules.length} added.`);
}
saveRules();
renderRules(); // Re-render the list
ruleContent.value = ''; // Clear the textarea
// Focus textarea again for quick multi-add
// ruleContent.focus();
}
function handleEditRule(event) {
const ruleItem = event.target.closest('.rule-item');
const index = parseInt(ruleItem.dataset.index);
ruleContent.value = rules[index];
ruleContent.dataset.editIndex = index; // Store index for update
addRuleText.textContent = 'Update Rule'; // Change button text
addRuleBtn.title = "Update rule";
ruleContent.focus(); // Focus textarea for editing
}
function handleDeleteRule(event) {
if (!confirm('Are you sure you want to delete this rule?')) {
return;
}
const ruleItem = event.target.closest('.rule-item');
const index = parseInt(ruleItem.dataset.index);
// Add deleting class for animation
ruleItem.classList.add('deleting');
// Wait for animation to finish before removing from data and DOM
setTimeout(() => {
rules.splice(index, 1);
saveRules();
// Re-render might be simpler than just removing the element,
// especially if indices need updating, but removing directly is smoother.
// ruleItem.remove(); // Remove directly from DOM
renderRules(); // Or re-render the whole list to update indices correctly
addSystemMessage(`Rule deleted.`);
}, 400); // Match animation duration
}
function exportRules() {
if (rules.length === 0) {
addSystemMessage("No rules to export.");
return;
}
const dataStr = JSON.stringify(rules, null, 2); // Pretty print JSON
const dataUri = 'data:application/json;charset=utf-8,' + encodeURIComponent(dataStr);
const exportFileDefaultName = `chatbot-rules-${new Date().toISOString().slice(0,10)}.json`;
const linkElement = document.createElement('a');
linkElement.setAttribute('href', dataUri);
linkElement.setAttribute('download', exportFileDefaultName);
linkElement.click();
linkElement.remove(); // Clean up
addSystemMessage("Rules exported successfully.");
}
function importRules(importedRules) {
if (!Array.isArray(importedRules) || !importedRules.every(item => typeof item === 'string')) {
addSystemMessage('Import failed: File does not contain a valid array of strings.');
console.error('Invalid import data:', importedRules);
return;
}
if (confirm(`Import ${importedRules.length} rules? This will replace all existing rules.`)) {
rules = importedRules;
saveRules();
renderRules();
addSystemMessage(`Successfully imported ${importedRules.length} rules.`);
}
}
// --- Drag and Drop ---
function setupDragAndDrop() {
const body = document.body;
body.addEventListener('dragenter', (e) => {
e.preventDefault();
e.stopPropagation();
body.classList.add('drag-active');
});
body.addEventListener('dragover', (e) => {
e.preventDefault(); // Necessary to allow drop
e.stopPropagation();
body.classList.add('drag-active'); // Keep class while hovering
});
body.addEventListener('dragleave', (e) => {
e.preventDefault();
e.stopPropagation();
// Only remove if leaving the window, not just hovering over child elements
if (e.relatedTarget === null || !body.contains(e.relatedTarget)) {
body.classList.remove('drag-active');
}
});
body.addEventListener('drop', (e) => {
e.preventDefault();
e.stopPropagation();
body.classList.remove('drag-active');
const file = e.dataTransfer.files[0];
if (file && file.type === 'application/json') {
const reader = new FileReader();
reader.onload = function(event) {
try {
const content = JSON.parse(event.target.result);
importRules(content); // Use the import function
} catch (error) {
console.error('Error parsing JSON file:', error);
addSystemMessage('Error importing rules: Could not parse JSON file. Ensure it is a valid JSON array of strings.');
}
};
reader.onerror = function() {
console.error('Error reading file:', reader.error);
addSystemMessage('Error reading the dropped file.');
}
reader.readAsText(file);
} else if (file) {
addSystemMessage('Import failed: Please drop a valid JSON file (.json).');
}
});
}
// --- Event Listeners ---
sendBtn.addEventListener('click', handleSendMessage);
userInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter' && !e.shiftKey) { // Send on Enter, allow Shift+Enter for newline
e.preventDefault(); // Prevent default newline in input
handleSendMessage();
}
});
addRuleBtn.addEventListener('click', handleAddOrUpdateRule);
clearFormBtn.addEventListener('click', () => {
ruleContent.value = '';
delete ruleContent.dataset.editIndex; // Clear edit state if any
addRuleText.textContent = 'Add Rule';
addRuleBtn.title = "Add rule";
});
exportRulesBtn.addEventListener('click', exportRules);
settingsBtn.addEventListener('click', openSettingsPanel);
closeSettingsBtn.addEventListener('click', closeSettingsPanel);
saveSettingsBtn.addEventListener('click', saveSettings);
// Close settings panel if clicking outside of it
document.addEventListener('click', (event) => {
if (!settingsPanel.classList.contains('visually-hidden') &&
!settingsPanel.contains(event.target) &&
event.target !== settingsBtn &&
!settingsBtn.contains(event.target)) {
closeSettingsPanel();
}
});
});
</script>
</body>
</html>