Spaces:
Sleeping
Sleeping
| <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 ; } | |
| .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 ; | |
| animation-iteration-count: 1 ; | |
| transition-duration: 0.01ms ; | |
| } | |
| } | |
| </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">×</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> |