EasyBot / templates /index.html
NitinBot001's picture
Upload index.html
db0be5f verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dhara Agri AI Assistant</title>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
:root {
--glass-bg: rgba(255, 255, 255, 0.15);
--glass-border: rgba(255, 255, 255, 0.25);
--glass-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
--text-primary: #1a1a1a;
--text-secondary: #4a5568;
--accent-primary: #10b981;
--accent-secondary: #059669;
--danger: #ef4444;
--success: #22c55e;
--backdrop: rgba(0, 0, 0, 0.3);
--font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
-webkit-tap-highlight-color: transparent;
}
body {
font-family: var(--font-family);
background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
background-attachment: fixed;
color: var(--text-primary);
display: flex;
height: 100vh;
overflow: hidden;
-webkit-font-smoothing: antialiased;
position: relative;
}
body::before {
content: '';
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1440 320"><path fill="%23ffffff" fill-opacity="0.05" d="M0,96L48,112C96,128,192,160,288,160C384,160,480,128,576,122.7C672,117,768,139,864,154.7C960,171,1056,181,1152,165.3C1248,149,1344,107,1392,85.3L1440,64L1440,320L1392,320C1344,320,1248,320,1152,320C1056,320,960,320,864,320C768,320,672,320,576,320C480,320,384,320,288,320C192,320,96,320,48,320L0,320Z"></path></svg>') no-repeat bottom;
background-size: cover;
pointer-events: none;
}
.app-container {
display: flex;
width: 100%;
height: 100%;
position: relative;
}
/* Glass Sidebar */
.sidebar {
position: fixed;
top: 0;
left: 0;
bottom: 0;
width: 85%;
max-width: 320px;
background: var(--glass-bg);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-right: 1px solid var(--glass-border);
display: flex;
flex-direction: column;
padding: 20px 16px;
transform: translateX(-100%);
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
z-index: 1000;
box-shadow: var(--glass-shadow);
}
.app-container.sidebar-visible .sidebar {
transform: translateX(0);
}
.sidebar-header {
display: flex;
gap: 12px;
align-items: center;
margin-bottom: 20px;
}
.sidebar-button {
flex: 1;
padding: 14px 20px;
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
border: none;
border-radius: 14px;
color: white;
cursor: pointer;
font-size: 1rem;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(16, 185, 129, 0.3);
min-height: 48px;
}
.sidebar-button:active {
transform: scale(0.98);
}
.close-sidebar-btn {
background: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(10px);
border: 1px solid var(--glass-border);
color: white;
cursor: pointer;
padding: 10px;
border-radius: 12px;
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
}
.close-sidebar-btn:active {
transform: scale(0.95);
background: rgba(255, 255, 255, 0.3);
}
/* Search and Filter */
.search-input {
width: 100%;
padding: 14px 16px;
margin-bottom: 12px;
border: 1px solid var(--glass-border);
border-radius: 12px;
font-size: 0.95rem;
font-family: var(--font-family);
background: rgba(255, 255, 255, 0.25);
backdrop-filter: blur(10px);
color: white;
outline: none;
transition: all 0.3s ease;
min-height: 48px;
}
.search-input::placeholder {
color: rgba(255, 255, 255, 0.7);
}
.search-input:focus {
background: rgba(255, 255, 255, 0.35);
border-color: var(--accent-primary);
box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.2);
}
.clear-all-btn {
width: 100%;
padding: 12px 16px;
margin-bottom: 16px;
background: rgba(239, 68, 68, 0.2);
backdrop-filter: blur(10px);
color: white;
border: 1px solid rgba(239, 68, 68, 0.4);
border-radius: 12px;
cursor: pointer;
font-size: 0.9rem;
font-weight: 500;
transition: all 0.3s ease;
min-height: 48px;
}
.clear-all-btn:active {
transform: scale(0.98);
background: rgba(239, 68, 68, 0.4);
}
/* Chat History */
.chat-history {
flex: 1;
overflow-y: auto;
margin-top: 8px;
padding-right: 4px;
-webkit-overflow-scrolling: touch;
}
.chat-history::-webkit-scrollbar {
width: 4px;
}
.chat-history::-webkit-scrollbar-track {
background: transparent;
}
.chat-history::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3);
border-radius: 2px;
}
.chat-history-item {
padding: 14px 16px;
margin-bottom: 8px;
border-radius: 12px;
cursor: pointer;
transition: all 0.3s ease;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border: 1px solid transparent;
font-size: 0.95rem;
color: white;
display: flex;
align-items: center;
justify-content: space-between;
min-height: 48px;
}
.chat-history-item:active {
transform: scale(0.98);
background: rgba(255, 255, 255, 0.2);
}
.chat-history-item.active {
background: linear-gradient(135deg, rgba(16, 185, 129, 0.3), rgba(5, 150, 105, 0.3));
border-color: var(--accent-primary);
font-weight: 600;
}
.chat-title {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.delete-chat-btn {
background: rgba(239, 68, 68, 0.3);
backdrop-filter: blur(10px);
border: none;
color: white;
cursor: pointer;
font-size: 20px;
padding: 4px;
width: 32px;
height: 32px;
line-height: 24px;
border-radius: 8px;
transition: all 0.3s ease;
opacity: 0.7;
flex-shrink: 0;
margin-left: 8px;
}
.chat-history-item:hover .delete-chat-btn,
.chat-history-item:active .delete-chat-btn {
opacity: 1;
}
.delete-chat-btn:active {
transform: scale(0.9);
background: rgba(239, 68, 68, 0.5);
}
/* User Stats */
.user-stats {
padding: 16px;
margin-top: 12px;
background: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(10px);
border-radius: 12px;
font-size: 0.85rem;
color: white;
border: 1px solid var(--glass-border);
line-height: 1.6;
}
/* Main Chat Area */
.chat-area {
width: 100%;
display: flex;
flex-direction: column;
position: relative;
}
.app-container.sidebar-visible .chat-area::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: var(--backdrop);
backdrop-filter: blur(3px);
z-index: 999;
}
/* Glass Header */
.chat-header {
display: flex;
align-items: center;
padding: 16px 20px;
background: var(--glass-bg);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-bottom: 1px solid var(--glass-border);
box-shadow: var(--glass-shadow);
min-height: 64px;
}
.menu-toggle {
background: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(10px);
border: 1px solid var(--glass-border);
color: white;
cursor: pointer;
padding: 10px;
border-radius: 12px;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
min-width: 48px;
min-height: 48px;
}
.menu-toggle:active {
transform: scale(0.95);
background: rgba(255, 255, 255, 0.3);
}
#chat-title {
margin-left: 16px;
font-size: 1.05rem;
font-weight: 600;
color: white;
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
}
/* Chat Messages with Overflow Fix */
.chat-messages {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
padding: 20px 16px 80px;
display: flex;
flex-direction: column;
gap: 16px;
-webkit-overflow-scrolling: touch;
}
.chat-messages::-webkit-scrollbar {
width: 6px;
}
.chat-messages::-webkit-scrollbar-track {
background: transparent;
}
.chat-messages::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3);
border-radius: 3px;
}
.welcome-message {
text-align: center;
margin: auto;
padding: 40px 20px;
}
.welcome-message h1 {
font-size: 2rem;
font-weight: 700;
color: white;
margin-bottom: 12px;
text-shadow: 0 2px 20px rgba(0, 0, 0, 0.2);
}
.welcome-message p {
font-size: 0.95rem;
color: rgba(255, 255, 255, 0.9);
font-weight: 400;
}
/* Glass Messages with Overflow Control */
.message {
max-width: 90%;
padding: 16px 18px;
border-radius: 18px;
line-height: 1.6;
word-wrap: break-word;
font-size: 1rem;
box-shadow: var(--glass-shadow);
transition: all 0.3s ease;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid var(--glass-border);
}
/* Message Content Wrapper - Critical for overflow */
.message > * {
max-width: 100%;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.message p {
margin: 0.6em 0;
}
.message ul, .message ol {
padding-left: 20px;
margin: 0.6em 0;
}
.message li {
margin: 0.4em 0;
}
/* Table Overflow Fix - Most Important */
.message table {
display: block;
width: 100%;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
border-collapse: collapse;
margin: 12px 0;
background: rgba(255, 255, 255, 0.05);
border-radius: 8px;
}
.message table::-webkit-scrollbar {
height: 6px;
}
.message table::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.1);
border-radius: 3px;
}
.message table::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3);
border-radius: 3px;
}
.message th,
.message td {
padding: 10px 12px;
text-align: left;
border: 1px solid rgba(255, 255, 255, 0.2);
white-space: nowrap;
min-width: 100px;
}
.message th {
background: rgba(255, 255, 255, 0.1);
font-weight: 600;
}
.message code {
background: rgba(0, 0, 0, 0.2);
padding: 3px 8px;
border-radius: 6px;
font-size: 0.9em;
border: 1px solid rgba(255, 255, 255, 0.1);
font-family: 'Monaco', 'Courier New', monospace;
display: inline-block;
max-width: 100%;
overflow-x: auto;
}
.message pre {
background: rgba(0, 0, 0, 0.3);
padding: 16px;
border-radius: 12px;
overflow-x: auto;
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: inset 0 2px 10px rgba(0, 0, 0, 0.2);
margin: 12px 0;
-webkit-overflow-scrolling: touch;
}
.message pre::-webkit-scrollbar {
height: 6px;
}
.message pre::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.2);
border-radius: 3px;
}
.message pre::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3);
border-radius: 3px;
}
.message pre code {
background: none;
border: none;
padding: 0;
display: block;
}
.message img.user-upload {
max-width: 100%;
height: auto;
border-radius: 12px;
margin-top: 12px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
}
.user-message {
background: linear-gradient(135deg, rgba(16, 185, 129, 0.3), rgba(5, 150, 105, 0.3));
align-self: flex-end;
border-bottom-right-radius: 6px;
color: white;
}
.assistant-message {
background: rgba(255, 255, 255, 0.25);
align-self: flex-start;
border-bottom-left-radius: 6px;
color: white;
}
.assistant-message.loading::after {
content: '...';
display: inline-block;
animation: blink 1.2s infinite;
}
@keyframes blink {
0%, 100% { opacity: 0.3; }
50% { opacity: 1; }
}
/* Glass Input Area - Fixed at Bottom */
.chat-input-area {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 16px;
background: var(--glass-bg);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-top: 1px solid var(--glass-border);
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.1);
z-index: 100;
}
.image-preview {
position: relative;
width: 100px;
margin-bottom: 12px;
}
#image-preview {
width: 100%;
border-radius: 12px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
}
#remove-image-btn {
position: absolute;
top: -8px;
right: -8px;
background: var(--danger);
color: white;
border: 2px solid white;
border-radius: 50%;
width: 32px;
height: 32px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
transition: all 0.3s ease;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
}
#remove-image-btn:active {
transform: scale(0.9);
}
.chat-input-wrapper {
display: flex;
align-items: flex-end;
gap: 10px;
background: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(10px);
border: 1px solid var(--glass-border);
border-radius: 16px;
padding: 8px;
transition: all 0.3s ease;
}
.chat-input-wrapper:focus-within {
background: rgba(255, 255, 255, 0.3);
border-color: var(--accent-primary);
box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.2);
}
#message-input {
flex: 1;
background: transparent;
border: none;
color: white;
font-size: 1rem;
font-family: var(--font-family);
padding: 12px 14px;
resize: none;
max-height: 120px;
overflow-y: auto;
outline: none;
line-height: 1.5;
min-height: 24px;
}
#message-input::placeholder {
color: rgba(255, 255, 255, 0.6);
}
.input-action-btn {
background: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(10px);
border: 1px solid var(--glass-border);
color: white;
cursor: pointer;
min-width: 48px;
min-height: 48px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 12px;
transition: all 0.3s ease;
flex-shrink: 0;
}
.input-action-btn:active {
transform: scale(0.95);
background: rgba(255, 255, 255, 0.3);
}
.send-btn {
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
border: none;
box-shadow: 0 4px 15px rgba(16, 185, 129, 0.3);
}
.send-btn:active {
transform: scale(0.95);
}
/* User Menu Button - Glass Style */
.user-menu-btn {
position: fixed;
top: 16px;
right: 16px;
background: var(--glass-bg);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
color: white;
border: 1px solid var(--glass-border);
border-radius: 14px;
width: 48px;
height: 48px;
cursor: pointer;
font-size: 20px;
z-index: 0;
box-shadow: var(--glass-shadow);
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
}
.user-menu-btn:active {
transform: scale(0.95);
background: rgba(255, 255, 255, 0.25);
}
.user-menu {
position: fixed;
top: 72px;
right: 16px;
background: var(--glass-bg);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid var(--glass-border);
border-radius: 16px;
padding: 20px;
box-shadow: var(--glass-shadow);
display: none;
z-index: 1002;
min-width: 240px;
max-width: calc(100vw - 32px);
}
.user-menu button {
width: 100%;
margin: 8px 0;
padding: 14px 16px;
border: 1px solid var(--glass-border);
border-radius: 12px;
cursor: pointer;
font-size: 0.95rem;
font-weight: 500;
font-family: var(--font-family);
transition: all 0.3s ease;
backdrop-filter: blur(10px);
min-height: 48px;
}
#export-data-btn {
background: rgba(16, 185, 129, 0.3);
color: white;
}
#export-data-btn:active {
transform: scale(0.98);
background: rgba(16, 185, 129, 0.5);
}
#delete-account-btn {
background: rgba(239, 68, 68, 0.3);
color: white;
}
#delete-account-btn:active {
transform: scale(0.98);
background: rgba(239, 68, 68, 0.5);
}
/* Desktop Optimizations */
@media (min-width: 768px) {
.chat-header { display: none; }
.sidebar {
position: static;
transform: translateX(0);
width: 300px;
max-width: 300px;
}
.close-sidebar-btn { display: none !important; }
.app-container.sidebar-visible .chat-area::before { display: none; }
.chat-area {
flex: 1;
}
.chat-messages {
padding: 40px 48px 100px;
}
.chat-input-area {
position: static;
padding: 24px 48px 32px;
}
.message {
max-width: 75%;
}
.user-menu-btn {
top: 24px;
right: 24px;
}
.user-menu {
top: 80px;
right: 24px;
}
}
/* Smooth Animations */
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.message {
animation: slideIn 0.3s ease;
}
/* Loading States */
.loading-shimmer {
background: linear-gradient(90deg,
rgba(255, 255, 255, 0.1) 0%,
rgba(255, 255, 255, 0.2) 50%,
rgba(255, 255, 255, 0.1) 100%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
/* Accessibility - High Contrast for readability */
@media (prefers-contrast: high) {
:root {
--glass-bg: rgba(255, 255, 255, 0.3);
--text-primary: #000;
}
.message {
border-width: 2px;
}
}
/* Reduce Motion */
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
</style>
</head>
<body>
<div id="app-container" class="app-container">
<aside class="sidebar">
<div class="sidebar-header">
<button id="new-chat-btn" class="sidebar-button">
<span class="icon">+</span> New Chat
</button>
<button id="close-sidebar-btn" class="close-sidebar-btn">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="24" height="24">
<path fill-rule="evenodd" d="M5.47 5.47a.75.75 0 011.06 0L12 10.94l5.47-5.47a.75.75 0 111.06 1.06L13.06 12l5.47 5.47a.75.75 0 11-1.06 1.06L12 13.06l-5.47 5.47a.75.75 0 01-1.06-1.06L10.94 12 5.47 6.53a.75.75 0 010-1.06z" clip-rule="evenodd" />
</svg>
</button>
</div>
<input type="text" class="search-input" placeholder="Search chats..." id="search-input">
<button class="clear-all-btn" id="clear-all-btn">Clear All My Chats</button>
<div class="chat-history" id="chat-history-list"></div>
<div class="user-stats" id="user-stats"></div>
</aside>
<main id="chat-area" class="chat-area">
<header class="chat-header">
<button id="menu-toggle" class="menu-toggle">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="24" height="24">
<path d="M3 6.75A.75.75 0 013.75 6h16.5a.75.75 0 010 1.5H3.75A.75.75 0 013 6.75zM3 12a.75.75 0 01.75-.75h16.5a.75.75 0 010 1.5H3.75A.75.75 0 013 12zm0 5.25a.75.75 0 01.75-.75h16.5a.75.75 0 010 1.5H3.75a.75.75 0 01-.75-.75z"></path>
</svg>
</button>
<h2 id="chat-title">New Chat</h2>
</header>
<div class="chat-messages" id="chat-messages">
<div class="welcome-message">
<h1>🌾 Dhara Agri AI</h1>
<p>Your smart farming assistant</p>
</div>
</div>
<div class="chat-input-area">
<div class="image-preview" id="image-preview-container" style="display: none;">
<img id="image-preview" src="#" alt="Image preview"/>
<button id="remove-image-btn">&times;</button>
</div>
<div class="chat-input-wrapper">
<input type="file" id="image-upload-input" accept="image/*" style="display: none;">
<button id="image-upload-btn" class="input-action-btn">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="22" height="22">
<path fill-rule="evenodd" d="M1.5 6a2.25 2.25 0 012.25-2.25h16.5A2.25 2.25 0 0122.5 6v12a2.25 2.25 0 01-2.25 2.25H3.75A2.25 2.25 0 011.5 18V6zM3 16.06V18c0 .414.336.75.75.75h16.5A.75.75 0 0021 18v-1.94l-2.69-2.689a1.5 1.5 0 00-2.12 0l-.88.879.97.97a.75.75 0 11-1.06 1.06l-5.16-5.159a1.5 1.5 0 00-2.12 0L3 16.061zm10.125-7.81a1.125 1.125 0 112.25 0 1.125 1.125 0 01-2.25 0z" clip-rule="evenodd"></path>
</svg>
</button>
<textarea id="message-input" placeholder="Ask about farming, crops..." rows="1"></textarea>
<button id="send-btn" class="input-action-btn send-btn">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="22" height="22">
<path d="M3.478 2.405a.75.75 0 00-.926.94l2.432 7.905H13.5a.75.75 0 010 1.5H4.984l-2.432 7.905a.75.75 0 00.926.94 60.519 60.519 0 0018.445-8.986.75.75 0 000-1.218A60.517 60.517 0 003.478 2.405z"></path>
</svg>
</button>
</div>
</div>
</main>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
// DOM Elements
const chatArea = document.getElementById('chat-area');
const closeSidebarBtn = document.getElementById('close-sidebar-btn');
const messageInput = document.getElementById('message-input');
const sendBtn = document.getElementById('send-btn');
const newChatBtn = document.getElementById('new-chat-btn');
const chatMessages = document.getElementById('chat-messages');
const chatHistoryList = document.getElementById('chat-history-list');
const menuToggle = document.getElementById('menu-toggle');
const appContainer = document.getElementById('app-container');
const chatTitle = document.getElementById('chat-title');
const imageUploadBtn = document.getElementById('image-upload-btn');
const imageUploadInput = document.getElementById('image-upload-input');
const imagePreviewContainer = document.getElementById('image-preview-container');
const imagePreview = document.getElementById('image-preview');
const removeImageBtn = document.getElementById('remove-image-btn');
const searchInput = document.getElementById('search-input');
const clearAllBtn = document.getElementById('clear-all-btn');
const userStatsDiv = document.getElementById('user-stats');
// State
let currentChatId = null;
let currentUserId = null;
let conversationsCache = {};
let selectedImageFile = null;
let imgbbApiKey = '';
let firebase = null;
let firebaseDB = null;
let useFirebase = false;
// User Management
const generateUserFingerprint = () => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
ctx.textBaseline = 'top';
ctx.font = '14px Arial';
ctx.fillText('Device fingerprint', 2, 2);
const fingerprint = [
navigator.userAgent,
navigator.language,
navigator.platform,
navigator.cookieEnabled,
navigator.doNotTrack,
screen.width + 'x' + screen.height,
screen.colorDepth,
new Date().getTimezoneOffset(),
canvas.toDataURL(),
navigator.hardwareConcurrency || 'unknown',
navigator.deviceMemory || 'unknown'
].join('|');
return hashCode(fingerprint).toString();
};
const hashCode = (str) => {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
return Math.abs(hash);
};
const initializeUser = async () => {
let userId = localStorage.getItem('easyfarms_user_id');
if (!userId) {
const deviceFingerprint = generateUserFingerprint();
const timestamp = Date.now();
const randomPart = Math.random().toString(36).substr(2, 8);
userId = `user_${deviceFingerprint}_${timestamp}_${randomPart}`;
localStorage.setItem('easyfarms_user_id', userId);
console.log('Generated new user ID:', userId);
}
currentUserId = userId;
if (useFirebase && firebaseDB) {
try {
await saveUserData(userId, {
created_at: new Date().toISOString(),
device_fingerprint: generateUserFingerprint(),
last_seen: new Date().toISOString()
});
} catch (error) {
console.error('Failed to save user data to Firebase:', error);
}
}
return userId;
};
const saveUserData = async (userId, userData) => {
try {
if (useFirebase && firebaseDB) {
const { setDoc, doc } = await import('https://www.gstatic.com/firebasejs/9.0.0/firebase-firestore.js');
await setDoc(doc(firebaseDB, 'users', userId), {
...userData,
updatedAt: new Date().toISOString()
});
} else {
const users = JSON.parse(localStorage.getItem('easyfarms_users') || '{}');
users[userId] = {
...userData,
updatedAt: new Date().toISOString()
};
localStorage.setItem('easyfarms_users', JSON.stringify(users));
}
} catch (error) {
console.error('Failed to save user data:', error);
}
};
// Storage Management
const saveSessionData = async (chatId, data) => {
try {
if (useFirebase && firebaseDB) {
const { setDoc, doc } = await import('https://www.gstatic.com/firebasejs/9.0.0/firebase-firestore.js');
await setDoc(doc(firebaseDB, 'user_sessions', `${currentUserId}_${chatId}`), {
...data,
userId: currentUserId,
chatId: chatId,
updatedAt: new Date().toISOString()
});
} else {
const userSessions = JSON.parse(localStorage.getItem(`easyfarms_sessions_${currentUserId}`) || '{}');
userSessions[chatId] = {
...data,
userId: currentUserId,
updatedAt: new Date().toISOString()
};
localStorage.setItem(`easyfarms_sessions_${currentUserId}`, JSON.stringify(userSessions));
}
} catch (error) {
console.error('Failed to save session data:', error);
}
};
const loadAllSessions = async () => {
try {
if (useFirebase && firebaseDB) {
const { getDocs, collection, query, where } = await import('https://www.gstatic.com/firebasejs/9.0.0/firebase-firestore.js');
const q = query(collection(firebaseDB, 'user_sessions'), where('userId', '==', currentUserId));
const querySnapshot = await getDocs(q);
const sessions = {};
querySnapshot.forEach((doc) => {
const data = doc.data();
sessions[data.chatId] = data;
});
return sessions;
} else {
return JSON.parse(localStorage.getItem(`easyfarms_sessions_${currentUserId}`) || '{}');
}
} catch (error) {
console.error('Failed to load all sessions:', error);
return JSON.parse(localStorage.getItem(`easyfarms_sessions_${currentUserId}`) || '{}');
}
};
const deleteSessionData = async (chatId) => {
try {
if (useFirebase && firebaseDB) {
const { deleteDoc, doc } = await import('https://www.gstatic.com/firebasejs/9.0.0/firebase-firestore.js');
await deleteDoc(doc(firebaseDB, 'user_sessions', `${currentUserId}_${chatId}`));
} else {
const userSessions = JSON.parse(localStorage.getItem(`easyfarms_sessions_${currentUserId}`) || '{}');
delete userSessions[chatId];
localStorage.setItem(`easyfarms_sessions_${currentUserId}`, JSON.stringify(userSessions));
}
} catch (error) {
console.error('Failed to delete session data:', error);
}
};
// Core Functions
const init = async () => {
await initializeUser();
await fetchConfig();
await loadCachedSessions();
await renderChatHistoryFromAPI();
setupEventListeners();
updateUserStats();
createUserMenu();
console.log('App initialized for user:', currentUserId);
};
const fetchConfig = async () => {
try {
const response = await fetch('https://nitinbot001-easybot.hf.space/config');
const config = await response.json();
imgbbApiKey = config.imgbb_api_key;
} catch (error) {
console.error('Failed to fetch config:', error);
}
};
const setupEventListeners = () => {
sendBtn.addEventListener('click', sendMessage);
messageInput.addEventListener('keydown', e => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
});
imageUploadBtn.addEventListener('click', () => imageUploadInput.click());
imageUploadInput.addEventListener('change', handleImageSelect);
removeImageBtn.addEventListener('click', removeSelectedImage);
newChatBtn.addEventListener('click', () => { startNewChat(); closeSidebar(); });
menuToggle.addEventListener('click', (e) => {
e.stopPropagation();
appContainer.classList.toggle('sidebar-visible');
});
closeSidebarBtn.addEventListener('click', (e) => {
e.stopPropagation();
closeSidebar();
});
chatArea.addEventListener('click', () => {
if (appContainer.classList.contains('sidebar-visible')) {
closeSidebar();
}
});
searchInput.addEventListener('input', filterChats);
clearAllBtn.addEventListener('click', confirmClearAllChats);
};
const sendMessage = async () => {
const messageText = messageInput.value.trim();
if (!messageText && !selectedImageFile) return;
displayMessage({
role: 'user',
content: messageText,
imageUrl: selectedImageFile
});
const loadingIndicator = displayMessage({
role: 'assistant',
content: 'Thinking...',
isLoading: true
});
try {
let permanentImageUrl = null;
if (selectedImageFile) {
permanentImageUrl = await uploadImageToImgBB(selectedImageFile);
}
const requestData = {
query: messageText,
session_id: currentChatId,
user_id: currentUserId
};
if (permanentImageUrl) {
requestData.image_url = permanentImageUrl;
}
const response = await fetch('https://nitinbot001-easybot.hf.space/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(requestData)
});
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
const data = await response.json();
chatMessages.removeChild(loadingIndicator);
displayMessage({
role: 'assistant',
content: data.response,
message_id: data.assistant_message_id
});
await updateCacheWithNewSystem(
data.chat_id,
{
content: messageText,
imageUrl: permanentImageUrl,
message_id: data.user_message_id
},
{
content: data.response,
message_id: data.assistant_message_id
},
data.is_new_chat
);
updateUserStats();
} catch (error) {
console.error('Error sending message:', error);
chatMessages.removeChild(loadingIndicator);
displayMessage({
role: 'assistant',
content: "I apologize, but I'm having trouble connecting right now. Please check your internet connection and try again.",
isError: true
});
} finally {
messageInput.value = '';
messageInput.style.height = 'auto';
removeSelectedImage();
}
};
const uploadImageToImgBB = async (imageFile) => {
if (!imgbbApiKey) throw new Error("ImgBB API Key not configured.");
const formData = new FormData();
formData.append('image', imageFile);
formData.append('key', imgbbApiKey);
const response = await fetch('https://api.imgbb.com/1/upload', {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.success) {
return result.data.url;
} else {
throw new Error(result.error.message || 'Image upload failed.');
}
};
const displayMessage = (message) => {
const { role, content, imageUrl, isLoading, isError, message_id } = message;
const sender = role || message.sender;
const messageDiv = document.createElement('div');
messageDiv.classList.add('message', `${sender}-message`);
if (message_id) messageDiv.dataset.messageId = message_id;
let htmlContent = '';
const imageSrc = (typeof imageUrl === 'object' && imageUrl instanceof File)
? URL.createObjectURL(imageUrl)
: imageUrl;
if (imageSrc) {
htmlContent += `<img src="${imageSrc}" alt="User upload" class="user-upload">`;
}
if (content) {
htmlContent += marked.parse(content);
}
messageDiv.innerHTML = htmlContent || (isLoading ? '...' : '');
if (isLoading) messageDiv.classList.add('loading');
if (isError) messageDiv.classList.add('error');
chatMessages.appendChild(messageDiv);
chatMessages.scrollTop = chatMessages.scrollHeight;
return messageDiv;
};
const startNewChat = () => {
currentChatId = null;
chatMessages.innerHTML = `<div class="welcome-message"><h1>🌾 Dhara Agri AI</h1><p>Your smart farming assistant</p></div>`;
chatTitle.textContent = "New Chat";
updateActiveChatItem();
};
const switchChat = async (chatId) => {
currentChatId = chatId;
chatMessages.innerHTML = '';
if (conversationsCache[chatId] && conversationsCache[chatId].messages) {
conversationsCache[chatId].messages.forEach(displayMessage);
chatTitle.textContent = conversationsCache[chatId].title || "Chat";
} else {
const loading = displayMessage({
role: 'assistant',
content: 'Loading chat history...',
isLoading: true
});
try {
const response = await fetch(`https://nitinbot001-easybot.hf.space/chat/${chatId}/messages?user_id=${currentUserId}`);
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
const messages = await response.json();
if (!conversationsCache[chatId]) conversationsCache[chatId] = {};
conversationsCache[chatId].messages = messages;
await saveSessionData(chatId, conversationsCache[chatId]);
chatMessages.removeChild(loading);
messages.forEach(displayMessage);
const sessionData = await loadSessionData(chatId);
chatTitle.textContent = sessionData?.title || conversationsCache[chatId]?.title || "Chat";
} catch (error) {
console.error('Failed to load chat history:', error);
chatMessages.removeChild(loading);
displayMessage({
role: 'assistant',
content: "Failed to load chat history. Please try again.",
isError: true
});
}
}
updateActiveChatItem();
closeSidebar();
};
const loadSessionData = async (chatId) => {
try {
if (useFirebase && firebaseDB) {
const { getDoc, doc } = await import('https://www.gstatic.com/firebasejs/9.0.0/firebase-firestore.js');
const docRef = doc(firebaseDB, 'user_sessions', `${currentUserId}_${chatId}`);
const docSnap = await getDoc(docRef);
if (docSnap.exists()) return docSnap.data();
} else {
const userSessions = JSON.parse(localStorage.getItem(`easyfarms_sessions_${currentUserId}`) || '{}');
return userSessions[chatId] || null;
}
return null;
} catch (error) {
const userSessions = JSON.parse(localStorage.getItem(`easyfarms_sessions_${currentUserId}`) || '{}');
return userSessions[chatId] || null;
}
};
const deleteChat = async (chatId, chatItem) => {
if (!confirm('Are you sure you want to delete this chat?')) return;
try {
const response = await fetch(`https://nitinbot001-easybot.hf.space/chat/${chatId}?user_id=${currentUserId}`, {
method: 'DELETE'
});
await deleteSessionData(chatId);
delete conversationsCache[chatId];
chatItem.remove();
if (currentChatId === chatId) startNewChat();
updateUserStats();
} catch (error) {
console.error('Error deleting chat:', error);
alert('Failed to delete chat. Please try again.');
}
};
const updateCacheWithNewSystem = async (chatId, userTurn, assistantTurn, isNewChat) => {
currentChatId = chatId;
if (isNewChat || !conversationsCache[chatId]) {
const title = (userTurn.content || "Image Query").substring(0, 30) + '...';
conversationsCache[chatId] = {
title,
messages: [],
created_at: new Date().toISOString(),
user_id: currentUserId
};
const item = createChatHistoryItem(chatId, title);
chatHistoryList.prepend(item);
}
const userMessage = {
role: 'user',
content: userTurn.content,
message_id: userTurn.message_id,
timestamp: new Date().toISOString()
};
if (userTurn.imageUrl) userMessage.imageUrl = userTurn.imageUrl;
const assistantMessage = {
role: 'assistant',
content: assistantTurn.content,
message_id: assistantTurn.message_id,
timestamp: new Date().toISOString()
};
conversationsCache[chatId].messages.push(userMessage, assistantMessage);
conversationsCache[chatId].updated_at = new Date().toISOString();
conversationsCache[chatId].user_id = currentUserId;
await saveSessionData(chatId, conversationsCache[chatId]);
updateActiveChatItem();
chatTitle.textContent = conversationsCache[chatId].title;
};
const createChatHistoryItem = (chatId, title) => {
const item = document.createElement('div');
item.className = 'chat-history-item';
item.dataset.sessionId = chatId;
const titleElement = document.createElement('span');
titleElement.textContent = title;
titleElement.className = 'chat-title';
const deleteBtn = document.createElement('button');
deleteBtn.innerHTML = '×';
deleteBtn.className = 'delete-chat-btn';
deleteBtn.title = 'Delete chat';
titleElement.addEventListener('click', () => switchChat(chatId));
deleteBtn.addEventListener('click', (e) => {
e.stopPropagation();
deleteChat(chatId, item);
});
item.appendChild(titleElement);
item.appendChild(deleteBtn);
return item;
};
const loadCachedSessions = async () => {
try {
conversationsCache = await loadAllSessions();
} catch (error) {
conversationsCache = {};
}
};
const renderChatHistoryFromAPI = async () => {
try {
const response = await fetch(`https://nitinbot001-easybot.hf.space/chats?user_id=${currentUserId}`);
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
const sessions = await response.json();
chatHistoryList.innerHTML = '';
sessions.sort((a, b) => new Date(b.updated_at) - new Date(a.updated_at));
sessions.forEach(session => {
if (!conversationsCache[session.session_id]) {
conversationsCache[session.session_id] = {};
}
conversationsCache[session.session_id].title = session.title;
conversationsCache[session.session_id].message_count = session.message_count;
conversationsCache[session.session_id].created_at = session.created_at;
conversationsCache[session.session_id].updated_at = session.updated_at;
conversationsCache[session.session_id].user_id = currentUserId;
const item = createChatHistoryItem(session.session_id, session.title);
chatHistoryList.appendChild(item);
});
for (const [chatId, data] of Object.entries(conversationsCache)) {
if (data.user_id === currentUserId) {
await saveSessionData(chatId, data);
}
}
updateActiveChatItem();
updateUserStats();
} catch (error) {
console.error("Failed to render chat history from API:", error);
for (const [chatId, data] of Object.entries(conversationsCache)) {
if (data.title && data.user_id === currentUserId) {
const item = createChatHistoryItem(chatId, data.title);
chatHistoryList.appendChild(item);
}
}
}
};
const updateUserStats = async () => {
try {
const response = await fetch(`https://nitinbot001-easybot.hf.space/users/${currentUserId}/stats`);
if (response.ok) {
const stats = await response.json();
userStatsDiv.innerHTML = `
<div style="font-weight: 600; color: white; margin-bottom: 10px; font-size: 0.95rem;">📊 Your Stats</div>
<div style="display: flex; flex-direction: column; gap: 6px; color: rgba(255,255,255,0.9);">
<div style="display: flex; justify-content: space-between;">
<span>Chats:</span>
<span style="font-weight: 600;">${stats.total_sessions || 0}</span>
</div>
<div style="display: flex; justify-content: space-between;">
<span>Messages:</span>
<span style="font-weight: 600;">${stats.total_messages || 0}</span>
</div>
<div style="display: flex; justify-content: space-between;">
<span>Recent:</span>
<span style="font-weight: 600;">${stats.recent_sessions_24h || 0}</span>
</div>
</div>
`;
}
} catch (error) {
const sessions = await loadAllSessions();
userStatsDiv.innerHTML = `
<div style="font-weight: 600; color: white; margin-bottom: 10px; font-size: 0.95rem;">📊 Local Stats</div>
<div style="color: rgba(255,255,255,0.9);">
<div style="display: flex; justify-content: space-between; margin-bottom: 6px;">
<span>Chats:</span>
<span style="font-weight: 600;">${Object.keys(sessions).length}</span>
</div>
<div style="font-size: 0.75rem; margin-top: 8px; opacity: 0.7;">ID: ${currentUserId.substr(0, 12)}...</div>
</div>
`;
}
};
const filterChats = () => {
const searchTerm = searchInput.value.toLowerCase();
const chatItems = chatHistoryList.querySelectorAll('.chat-history-item');
chatItems.forEach(item => {
const title = item.textContent.toLowerCase();
item.style.display = title.includes(searchTerm) ? 'flex' : 'none';
});
};
const confirmClearAllChats = () => {
if (confirm('Are you sure you want to delete all your chats? This action cannot be undone.')) {
clearAllChats();
}
};
const clearAllChats = async () => {
try {
await fetch(`https://nitinbot001-easybot.hf.space/users/${currentUserId}/chats`, {
method: 'DELETE'
});
const allSessions = await loadAllSessions();
for (const chatId of Object.keys(allSessions)) {
await deleteSessionData(chatId);
}
conversationsCache = {};
chatHistoryList.innerHTML = '';
startNewChat();
updateUserStats();
} catch (error) {
console.error('Failed to clear all chats:', error);
}
};
const handleImageSelect = (event) => {
const file = event.target.files[0];
if (file) {
selectedImageFile = file;
imagePreview.src = URL.createObjectURL(file);
imagePreviewContainer.style.display = 'block';
}
};
const removeSelectedImage = () => {
selectedImageFile = null;
imageUploadInput.value = '';
imagePreviewContainer.style.display = 'none';
imagePreview.src = '#';
};
const updateActiveChatItem = () => {
document.querySelectorAll('.chat-history-item').forEach(item => {
item.classList.toggle('active', item.dataset.sessionId === currentChatId);
});
};
const closeSidebar = () => appContainer.classList.remove('sidebar-visible');
const createUserMenu = () => {
const userMenuBtn = document.createElement('button');
userMenuBtn.className = 'user-menu-btn';
userMenuBtn.innerHTML = '⚙️';
userMenuBtn.title = 'Settings';
const userMenu = document.createElement('div');
userMenu.className = 'user-menu';
userMenu.innerHTML = `
<div style="margin-bottom: 12px; font-weight: 600; font-size: 1rem; color: white;">Settings</div>
<div style="margin-bottom: 12px; font-size: 0.85rem; color: rgba(255,255,255,0.8); padding: 10px; background: rgba(255,255,255,0.1); border-radius: 8px; word-break: break-all;">
ID: ${currentUserId.substr(0, 20)}...
</div>
<button id="export-data-btn">📥 Export My Data</button>
<button id="delete-account-btn">🗑️ Delete Account</button>
`;
document.body.appendChild(userMenuBtn);
document.body.appendChild(userMenu);
userMenuBtn.addEventListener('click', (e) => {
e.stopPropagation();
userMenu.style.display = userMenu.style.display === 'none' ? 'block' : 'none';
});
document.addEventListener('click', () => {
userMenu.style.display = 'none';
});
document.getElementById('export-data-btn').addEventListener('click', exportUserData);
document.getElementById('delete-account-btn').addEventListener('click', deleteUserAccount);
};
const exportUserData = async () => {
try {
const response = await fetch(`https://nitinbot001-easybot.hf.space/users/${currentUserId}/export`);
if (response.ok) {
const userData = await response.json();
const dataStr = JSON.stringify(userData, null, 2);
const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr);
const linkElement = document.createElement('a');
linkElement.setAttribute('href', dataUri);
linkElement.setAttribute('download', `easyfarms_data_${currentUserId.substr(0, 8)}_${new Date().toISOString().split('T')[0]}.json`);
linkElement.click();
}
} catch (error) {
console.error('Failed to export user data:', error);
alert('Failed to export data. Please try again.');
}
};
const deleteUserAccount = async () => {
if (!confirm('Are you sure you want to delete your account? This will permanently delete all your chats and data.')) {
return;
}
if (prompt('Type "DELETE" to confirm account deletion:') !== 'DELETE') {
return;
}
try {
const response = await fetch(`https://nitinbot001-easybot.hf.space/users/${currentUserId}`, {
method: 'DELETE'
});
if (response.ok) {
localStorage.removeItem('easyfarms_user_id');
localStorage.removeItem(`easyfarms_sessions_${currentUserId}`);
localStorage.removeItem(`easyfarms_users`);
if (useFirebase && firebaseDB) {
try {
const { deleteDoc, doc, collection, getDocs, query, where } = await import('https://www.gstatic.com/firebasejs/9.0.0/firebase-firestore.js');
await deleteDoc(doc(firebaseDB, 'users', currentUserId));
const q = query(collection(firebaseDB, 'user_sessions'), where('userId', '==', currentUserId));
const querySnapshot = await getDocs(q);
querySnapshot.forEach(async (doc) => {
await deleteDoc(doc.ref);
});
} catch (firebaseError) {
console.error('Failed to clean Firebase data:', firebaseError);
}
}
alert('Account deleted successfully. Refreshing page...');
location.reload();
} else {
throw new Error('Account deletion failed');
}
} catch (error) {
console.error('Failed to delete account:', error);
alert('Failed to delete account. Please try again.');
}
};
// Auto-resize textarea
messageInput.addEventListener('input', function() {
this.style.height = 'auto';
this.style.height = Math.min(this.scrollHeight, 120) + 'px';
});
// Swipe to close sidebar on mobile
let touchStartX = 0;
let touchEndX = 0;
const sidebar = document.querySelector('.sidebar');
sidebar.addEventListener('touchstart', (e) => {
touchStartX = e.changedTouches[0].screenX;
});
sidebar.addEventListener('touchend', (e) => {
touchEndX = e.changedTouches[0].screenX;
if (touchStartX - touchEndX > 50) {
closeSidebar();
}
});
// Pull to refresh simulation (visual feedback only)
let pullStartY = 0;
let isPulling = false;
chatMessages.addEventListener('touchstart', (e) => {
if (chatMessages.scrollTop === 0) {
pullStartY = e.touches[0].clientY;
isPulling = true;
}
});
chatMessages.addEventListener('touchmove', (e) => {
if (isPulling && chatMessages.scrollTop === 0) {
const pullDistance = e.touches[0].clientY - pullStartY;
if (pullDistance > 80) {
renderChatHistoryFromAPI();
isPulling = false;
}
}
});
chatMessages.addEventListener('touchend', () => {
isPulling = false;
});
// Haptic feedback simulation (visual scale effect)
const addHapticFeedback = (element) => {
element.addEventListener('touchstart', function() {
this.style.transform = 'scale(0.98)';
});
element.addEventListener('touchend', function() {
this.style.transform = '';
});
};
// Apply haptic feedback to all buttons
document.querySelectorAll('button').forEach(addHapticFeedback);
// Prevent zoom on double tap for iOS
let lastTouchEnd = 0;
document.addEventListener('touchend', (e) => {
const now = Date.now();
if (now - lastTouchEnd <= 300) {
e.preventDefault();
}
lastTouchEnd = now;
}, false);
// Service Worker registration for PWA capabilities (optional)
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js').catch(() => {
console.log('Service Worker registration skipped');
});
}
// Online/Offline detection
window.addEventListener('online', () => {
console.log('Connection restored');
renderChatHistoryFromAPI();
});
window.addEventListener('offline', () => {
console.log('Connection lost - using offline mode');
});
// Visibility change - update stats when user returns
document.addEventListener('visibilitychange', () => {
if (!document.hidden) {
updateUserStats();
}
});
// Initialize app
init().catch((error) => {
console.error('Failed to initialize application:', error);
document.body.innerHTML = `
<div style="padding: 40px 20px; text-align: center; color: white;">
<h2 style="margin-bottom: 16px; font-size: 1.5rem;">Failed to Load</h2>
<p style="margin-bottom: 20px;">Please refresh the page and try again.</p>
<p style="color: rgba(255,255,255,0.7); font-size: 0.85rem;">Error: ${error.message}</p>
<button onclick="location.reload()" style="margin-top: 20px; padding: 12px 24px; background: linear-gradient(135deg, #10b981, #059669); border: none; border-radius: 12px; color: white; font-size: 1rem; font-weight: 600; cursor: pointer;">Refresh Page</button>
</div>
`;
});
});
</script>
</body>
</html>