Spaces:
Runtime error
Runtime error
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| <title>AI Chatbot Assistant</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link | |
| href="https://cdn.jsdelivr.net/npm/daisyui@3.1.0/dist/full.css" | |
| rel="stylesheet" | |
| type="text/css" | |
| /> | |
| <link | |
| href="https://fonts.googleapis.com/icon?family=Material+Icons" | |
| rel="stylesheet" | |
| /> | |
| <link | |
| href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" | |
| rel="stylesheet" | |
| /> | |
| <script src="https://www.gstatic.com/firebasejs/8.6.8/firebase-app.js"></script> | |
| <script src="https://www.gstatic.com/firebasejs/8.6.8/firebase-auth.js"></script> | |
| <script src="https://www.gstatic.com/firebasejs/8.6.8/firebase-database.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> | |
| <link | |
| rel="stylesheet" | |
| href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" | |
| /> | |
| <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script> | |
| <style> | |
| body { | |
| font-family: "Inter", sans-serif; | |
| } | |
| .chat-bubble-enter-active, | |
| .chat-bubble-leave-active { | |
| transition: all 0.3s ease; | |
| } | |
| .chat-bubble-enter, | |
| .chat-bubble-leave-to { | |
| opacity: 0; | |
| transform: translateY(20px); | |
| } | |
| .typing-indicator { | |
| display: inline-flex; | |
| align-items: center; | |
| padding: 12px 16px; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| border-radius: 18px; | |
| color: white; | |
| } | |
| .typing-indicator span { | |
| height: 8px; | |
| width: 8px; | |
| background: white; | |
| border-radius: 50%; | |
| display: inline-block; | |
| margin: 0 2px; | |
| animation: bounce 1.4s infinite ease-in-out both; | |
| } | |
| .typing-indicator span:nth-child(1) { | |
| animation-delay: -0.32s; | |
| } | |
| .typing-indicator span:nth-child(2) { | |
| animation-delay: -0.16s; | |
| } | |
| @keyframes bounce { | |
| 0%, | |
| 80%, | |
| 100% { | |
| transform: scale(0.8); | |
| opacity: 0.5; | |
| } | |
| 40% { | |
| transform: scale(1); | |
| opacity: 1; | |
| } | |
| } | |
| .gradient-bg { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| } | |
| .message-area { | |
| height: calc(100vh - 200px); | |
| overflow-y: auto; | |
| scrollbar-width: thin; | |
| scrollbar-color: #cbd5e0 #f7fafc; | |
| } | |
| .message-area::-webkit-scrollbar { | |
| width: 6px; | |
| } | |
| .message-area::-webkit-scrollbar-track { | |
| background: #f7fafc; | |
| } | |
| .message-area::-webkit-scrollbar-thumb { | |
| background: #cbd5e0; | |
| border-radius: 3px; | |
| } | |
| .keyword-tag { | |
| display: inline-flex; | |
| align-items: center; | |
| background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%); | |
| color: #0069d9; | |
| padding: 0.25rem 0.75rem; | |
| border-radius: 9999px; | |
| font-size: 0.75rem; | |
| font-weight: 500; | |
| margin: 0.25rem; | |
| } | |
| .keyword-tag button { | |
| background: none; | |
| border: none; | |
| color: #0069d9; | |
| cursor: pointer; | |
| margin-left: 0.25rem; | |
| padding: 0; | |
| font-size: 1rem; | |
| line-height: 1; | |
| } | |
| .dataset-item { | |
| background: #f8f9fa; | |
| border-radius: 8px; | |
| padding: 0.75rem; | |
| margin-bottom: 0.5rem; | |
| border-left: 3px solid #667eea; | |
| } | |
| .model-card { | |
| transition: all 0.3s ease; | |
| } | |
| .model-card:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 10px 25px rgba(102, 126, 234, 0.15); | |
| } | |
| .toggle-switch { | |
| position: relative; | |
| width: 48px; | |
| height: 24px; | |
| } | |
| .toggle-switch input { | |
| opacity: 0; | |
| width: 0; | |
| height: 0; | |
| } | |
| .toggle-switch .slider { | |
| position: absolute; | |
| cursor: pointer; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background-color: #cbd5e0; | |
| transition: 0.3s; | |
| border-radius: 24px; | |
| } | |
| .toggle-switch .slider:before { | |
| position: absolute; | |
| content: ""; | |
| height: 18px; | |
| width: 18px; | |
| left: 3px; | |
| bottom: 3px; | |
| background-color: white; | |
| transition: 0.3s; | |
| border-radius: 50%; | |
| } | |
| .toggle-switch input:checked + .slider { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| } | |
| .toggle-switch input:checked + .slider:before { | |
| transform: translateX(24px); | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-50"> | |
| <div id="app"> | |
| <div class="min-h-screen flex flex-col"> | |
| <!-- Header --> | |
| <header class="gradient-bg shadow-lg"> | |
| <div class="container mx-auto px-4 py-4"> | |
| <div class="flex items-center justify-between"> | |
| <div class="flex items-center space-x-3"> | |
| <span class="material-icons text-white text-3xl" | |
| >smart_toy</span | |
| > | |
| <h1 class="text-2xl font-bold text-white">AI Chat Assistant</h1> | |
| </div> | |
| <div class="flex items-center space-x-4"> | |
| <!-- Navigation Links --> | |
| <a | |
| href="/" | |
| class="btn btn-sm btn-ghost text-white hover:bg-white/20 bg-white/20" | |
| > | |
| <span class="material-icons">chat</span> | |
| <span class="hidden sm:inline">Chat</span> | |
| </a> | |
| <a | |
| href="/models" | |
| class="btn btn-sm btn-ghost text-white hover:bg-white/20" | |
| > | |
| <span class="material-icons">psychology</span> | |
| <span class="hidden sm:inline">Models</span> | |
| </a> | |
| <span | |
| id="statsIcon" | |
| class="material-icons text-white cursor-pointer hover:scale-110 transition-transform" | |
| >analytics</span | |
| > | |
| <span | |
| id="mapIcon" | |
| class="material-icons text-white cursor-pointer hover:scale-110 transition-transform" | |
| >map</span | |
| > | |
| <span | |
| id="modelsBtn" | |
| class="material-icons text-white cursor-pointer hover:scale-110 transition-transform" | |
| style="display: none" | |
| >psychology</span | |
| > | |
| <button | |
| id="logoutBtn" | |
| style="display: none" | |
| class="btn btn-sm btn-error text-white" | |
| > | |
| <span class="material-icons text-sm">logout</span> Logout | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </header> | |
| <!-- Main Content --> | |
| <main class="flex-1 container mx-auto px-4 py-6"> | |
| <!-- Stats Modal --> | |
| <div | |
| id="statsModal" | |
| style="display: none" | |
| class="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4" | |
| > | |
| <div | |
| class="bg-white rounded-lg p-6 max-w-2xl w-full max-h-[80vh] overflow-y-auto" | |
| > | |
| <div class="flex justify-between items-center mb-4"> | |
| <h2 class="text-xl font-bold">Chat Analytics</h2> | |
| <button id="closeStatsBtn" class="btn btn-sm btn-circle"> | |
| <span class="material-icons">close</span> | |
| </button> | |
| </div> | |
| <canvas id="statsChart" width="400" height="200"></canvas> | |
| </div> | |
| </div> | |
| <!-- Map Modal --> | |
| <div | |
| id="mapModal" | |
| style="display: none" | |
| class="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4" | |
| > | |
| <div class="bg-white rounded-lg p-6 max-w-2xl w-full"> | |
| <div class="flex justify-between items-center mb-4"> | |
| <h2 class="text-xl font-bold">Location</h2> | |
| <button id="closeMapBtn" class="btn btn-sm btn-circle"> | |
| <span class="material-icons">close</span> | |
| </button> | |
| </div> | |
| <div | |
| id="map" | |
| style="height: 400px; width: 100%; border-radius: 8px" | |
| ></div> | |
| </div> | |
| </div> | |
| <!-- Models Management Modal --> | |
| <div | |
| id="modelsModal" | |
| style="display: none" | |
| class="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4" | |
| > | |
| <div | |
| class="bg-white rounded-lg p-6 max-w-4xl w-full max-h-[90vh] overflow-y-auto" | |
| > | |
| <div class="flex justify-between items-center mb-6"> | |
| <h2 class="text-2xl font-bold">AI Models Management</h2> | |
| <button id="closeModelsBtn" class="btn btn-sm btn-circle"> | |
| <span class="material-icons">close</span> | |
| </button> | |
| </div> | |
| <!-- Model Search Section --> | |
| <div | |
| class="bg-blue-50 rounded-lg p-4 mb-6 border border-blue-200" | |
| > | |
| <div class="flex items-center gap-2 mb-3"> | |
| <span class="material-icons text-blue-600">search</span> | |
| <h3 class="font-semibold text-blue-900"> | |
| Search HuggingFace Models | |
| </h3> | |
| <button | |
| id="refreshModelsBtn" | |
| onclick="refreshModelsRealtime()" | |
| class="btn btn-sm btn-ghost ml-auto" | |
| title="Refresh model info from HuggingFace" | |
| > | |
| <span class="material-icons text-sm">refresh</span> | |
| Refresh Models | |
| </button> | |
| </div> | |
| <div class="flex gap-2"> | |
| <input | |
| id="modelSearchInput" | |
| type="text" | |
| class="input input-bordered input-sm flex-1" | |
| placeholder="Search models (e.g., 'code', 'chat', 'image')..." | |
| /> | |
| <button | |
| id="searchModelsBtn" | |
| onclick="searchHFModels()" | |
| class="btn btn-primary btn-sm" | |
| > | |
| <span class="material-icons text-sm">search</span> | |
| Search | |
| </button> | |
| </div> | |
| <div id="searchResultsContainer" class="hidden mt-3"></div> | |
| </div> | |
| <!-- Add/Edit Model Form --> | |
| <div class="bg-gray-50 rounded-lg p-6 mb-6"> | |
| <h3 id="modelFormTitle" class="font-semibold text-lg mb-4"> | |
| Add New Model | |
| </h3> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| <div class="form-control"> | |
| <label class="label" | |
| ><span class="label-text">Model Name *</span></label | |
| > | |
| <input | |
| id="modelName" | |
| type="text" | |
| class="input input-bordered" | |
| placeholder="e.g., Code Assistant" | |
| /> | |
| </div> | |
| <div class="form-control"> | |
| <label class="label" | |
| ><span class="label-text" | |
| >HuggingFace Model ID *</span | |
| ></label | |
| > | |
| <div class="flex gap-2"> | |
| <input | |
| id="modelId" | |
| type="text" | |
| class="input input-bordered flex-1" | |
| placeholder="e.g., meta-llama/Llama-3.2-3B-Instruct" | |
| value="meta-llama/Llama-3.2-3B-Instruct" | |
| /> | |
| <button | |
| id="fetchModelInfoBtn" | |
| type="button" | |
| class="btn btn-outline btn-sm" | |
| > | |
| <span class="material-icons text-sm">sync</span> | |
| Fetch | |
| </button> | |
| </div> | |
| <div id="modelInfoStatus" class="text-xs mt-1"></div> | |
| </div> | |
| <div class="form-control"> | |
| <label class="label" | |
| ><span class="label-text">Role *</span></label | |
| > | |
| <select id="modelRole" class="select select-bordered"> | |
| <option value="assistant">Assistant</option> | |
| <option value="expert">Expert</option> | |
| <option value="coder">Coder</option> | |
| <option value="analyst">Analyst</option> | |
| <option value="teacher">Teacher</option> | |
| <option value="creative">Creative</option> | |
| <option value="custom">Custom</option> | |
| </select> | |
| </div> | |
| <div class="form-control"> | |
| <label class="label" | |
| ><span class="label-text">Temperature</span></label | |
| > | |
| <input | |
| id="modelTemp" | |
| type="range" | |
| min="0" | |
| max="1" | |
| step="0.1" | |
| value="0.3" | |
| class="range" | |
| /> | |
| <span id="tempValue" class="text-xs text-gray-500" | |
| >0.3</span | |
| > | |
| </div> | |
| <div class="form-control"> | |
| <label class="label" | |
| ><span class="label-text">Max Tokens</span></label | |
| > | |
| <input | |
| id="modelMaxTokens" | |
| type="number" | |
| class="input input-bordered" | |
| placeholder="500" | |
| value="500" | |
| /> | |
| </div> | |
| <div class="form-control flex items-center pt-6"> | |
| <label class="flex items-center cursor-pointer"> | |
| <div class="toggle-switch"> | |
| <input id="modelEnabled" type="checkbox" checked /> | |
| <span class="slider"></span> | |
| </div> | |
| <span class="ml-3">Enable Model</span> | |
| </label> | |
| </div> | |
| <div class="form-control md:col-span-2"> | |
| <label class="label" | |
| ><span class="label-text">System Prompt</span></label | |
| > | |
| <textarea | |
| id="modelSystemPrompt" | |
| class="textarea textarea-bordered h-24" | |
| placeholder="Define the system behavior for this model..." | |
| ></textarea> | |
| </div> | |
| <!-- Keywords Section --> | |
| <div class="form-control md:col-span-2"> | |
| <label class="label"> | |
| <span class="label-text">Keywords</span> | |
| <span class="label-text-alt text-gray-400" | |
| >Auto-synced from HuggingFace</span | |
| > | |
| </label> | |
| <div | |
| id="keywordsContainer" | |
| class="flex flex-wrap gap-2 p-3 bg-white border rounded-lg min-h-[60px]" | |
| > | |
| <span class="text-gray-400 text-sm" | |
| >Click "Fetch" to load keywords from HuggingFace</span | |
| > | |
| </div> | |
| <div class="flex gap-2 mt-2"> | |
| <input | |
| id="customKeyword" | |
| type="text" | |
| class="input input-bordered input-sm flex-1" | |
| placeholder="Add custom keyword..." | |
| /> | |
| <button | |
| id="addKeywordBtn" | |
| type="button" | |
| class="btn btn-outline btn-sm" | |
| > | |
| <span class="material-icons text-sm">add</span> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Model Info Panel --> | |
| <div | |
| id="modelInfoPanel" | |
| class="md:col-span-2 bg-blue-50 rounded-lg p-4 hidden" | |
| > | |
| <h4 class="font-semibold text-blue-800 mb-2"> | |
| <span class="material-icons text-sm align-middle mr-1" | |
| >info</span | |
| > | |
| Model Information | |
| </h4> | |
| <div | |
| id="modelInfoContent" | |
| class="grid grid-cols-2 md:grid-cols-4 gap-3 text-sm" | |
| ></div> | |
| </div> | |
| </div> | |
| <div class="flex gap-2 mt-4"> | |
| <button id="saveModelBtn" class="btn btn-primary flex-1"> | |
| <span class="material-icons text-sm mr-1">add</span> | |
| Add Model | |
| </button> | |
| <button | |
| id="cancelEditBtn" | |
| style="display: none" | |
| class="btn btn-ghost" | |
| > | |
| Cancel | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Models List --> | |
| <div id="modelsList"></div> | |
| </div> | |
| </div> | |
| <!-- Preset Manager Modal --> | |
| <div | |
| id="presetManagerModal" | |
| style="display: none" | |
| class="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4" | |
| > | |
| <div | |
| class="bg-white rounded-lg p-6 max-w-4xl w-full max-h-[90vh] overflow-y-auto" | |
| > | |
| <div class="flex justify-between items-center mb-6"> | |
| <h2 class="text-2xl font-bold flex items-center"> | |
| <span class="material-icons mr-2">person_pin</span> | |
| Preset Manager | |
| </h2> | |
| <button id="closePresetManagerBtn" class="btn btn-sm btn-circle"> | |
| <span class="material-icons">close</span> | |
| </button> | |
| </div> | |
| <div class="mb-4 flex justify-end"> | |
| <button | |
| id="newPresetBtn" | |
| class="btn btn-primary" | |
| > | |
| <span class="material-icons text-sm mr-1">add</span> | |
| Create New Preset | |
| </button> | |
| </div> | |
| <div id="presetManagerContent"> | |
| <div class="text-center py-8 text-gray-500">Loading presets...</div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Preset Editor Modal --> | |
| <div | |
| id="presetsModal" | |
| style="display: none" | |
| class="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4" | |
| > | |
| <div | |
| class="bg-white rounded-lg p-6 max-w-2xl w-full max-h-[90vh] overflow-y-auto" | |
| > | |
| <div class="flex justify-between items-center mb-6"> | |
| <h2 class="text-2xl font-bold flex items-center"> | |
| <span class="material-icons mr-2">edit</span> | |
| <span id="presetModalTitle">Create New Preset</span> | |
| </h2> | |
| <button id="closePresetsModalBtn" class="btn btn-sm btn-circle"> | |
| <span class="material-icons">close</span> | |
| </button> | |
| </div> | |
| <div class="space-y-4"> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| <div class="form-control"> | |
| <label class="label"><span class="label-text">Name *</span></label> | |
| <input | |
| id="presetName" | |
| type="text" | |
| class="input input-bordered" | |
| placeholder="e.g., Code Expert" | |
| /> | |
| </div> | |
| <div class="form-control"> | |
| <label class="label"><span class="label-text">Icon</span></label> | |
| <input | |
| id="presetIcon" | |
| type="text" | |
| class="input input-bordered" | |
| placeholder="🎯" | |
| value="🎯" | |
| maxlength="2" | |
| /> | |
| </div> | |
| </div> | |
| <div class="form-control"> | |
| <label class="label"><span class="label-text">Description</span></label> | |
| <input | |
| id="presetDescription" | |
| type="text" | |
| class="input input-bordered" | |
| placeholder="Brief description of this preset..." | |
| /> | |
| </div> | |
| <div class="form-control"> | |
| <label class="label"><span class="label-text">HuggingFace Model ID *</span></label> | |
| <input | |
| id="presetModelId" | |
| type="text" | |
| class="input input-bordered" | |
| placeholder="meta-llama/Llama-3.2-3B-Instruct" | |
| value="meta-llama/Llama-3.2-3B-Instruct" | |
| /> | |
| </div> | |
| <div class="form-control"> | |
| <label class="label"><span class="label-text">System Prompt *</span></label> | |
| <textarea | |
| id="presetSystemPrompt" | |
| class="textarea textarea-bordered h-24" | |
| placeholder="Define the AI's behavior and personality..." | |
| ></textarea> | |
| </div> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| <div class="form-control"> | |
| <label class="label"><span class="label-text">Temperature</span></label> | |
| <input | |
| id="presetTemperature" | |
| type="range" | |
| min="0.1" | |
| max="1" | |
| step="0.1" | |
| value="0.5" | |
| class="range" | |
| /> | |
| <span id="presetTempValue" class="text-xs text-gray-500" | |
| >0.5</span | |
| > | |
| </div> | |
| <div class="form-control"> | |
| <label class="label"><span class="label-text">Max Tokens</span></label> | |
| <input | |
| id="presetMaxTokens" | |
| type="number" | |
| class="input input-bordered" | |
| placeholder="500" | |
| value="500" | |
| /> | |
| </div> | |
| </div> | |
| <!-- Keywords Section --> | |
| <div class="form-control"> | |
| <label class="label"> | |
| <span class="label-text">Keywords for Auto-Routing</span> | |
| <span class="label-text-alt text-gray-400" | |
| >Add keywords to automatically select this preset</span | |
| > | |
| </label> | |
| <div | |
| id="presetKeywordsContainer" | |
| class="flex flex-wrap gap-2 p-3 bg-white border rounded-lg min-h-[60px]" | |
| > | |
| <span class="text-gray-400 text-sm">No keywords</span> | |
| </div> | |
| <div class="flex gap-2 mt-2"> | |
| <input | |
| id="presetKeywordInput" | |
| type="text" | |
| class="input input-bordered input-sm flex-1" | |
| placeholder="Add keyword..." | |
| /> | |
| <button | |
| id="addPresetKeywordBtn" | |
| type="button" | |
| class="btn btn-outline btn-sm" | |
| > | |
| <span class="material-icons text-sm">add</span> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Datasets Section --> | |
| <div class="form-control"> | |
| <label class="label"> | |
| <span class="label-text font-medium cursor-pointer" onclick="toggleDatasetAccordion()"> | |
| <span class="material-icons text-sm align-middle">folder_open</span> | |
| Datasets (Optional) | |
| </label> | |
| </label> | |
| <div id="presetDatasetAccordion" class="hidden bg-gray-50 rounded-lg p-4"> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-2 mb-2"> | |
| <input | |
| id="presetDatasetName" | |
| type="text" | |
| class="input input-bordered input-sm" | |
| placeholder="Dataset name..." | |
| /> | |
| <input | |
| id="presetDatasetUrl" | |
| type="text" | |
| class="input input-bordered input-sm" | |
| placeholder="Dataset URL..." | |
| /> | |
| </div> | |
| <button | |
| id="addPresetDatasetBtn" | |
| type="button" | |
| class="btn btn-outline btn-sm" | |
| > | |
| <span class="material-icons text-sm mr-1">add</span> | |
| Add Dataset | |
| </button> | |
| <div id="presetDatasetsContainer" class="mt-3"> | |
| <span class="text-gray-400 text-sm">No datasets added</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="flex gap-2 pt-4 border-t"> | |
| <button id="presetSaveBtn" class="btn btn-primary flex-1"> | |
| <span class="material-icons text-sm mr-1">save</span> | |
| Save Preset | |
| </button> | |
| <button id="presetCancelBtn" class="btn btn-ghost"> | |
| Cancel | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Auth Section --> | |
| <div id="authSection" class="max-w-md mx-auto"> | |
| <div class="bg-white rounded-lg shadow-lg p-8"> | |
| <div class="text-center mb-6"> | |
| <span class="material-icons text-6xl text-purple-500" | |
| >chat</span | |
| > | |
| <h2 class="text-2xl font-bold mt-4">Welcome to AI Chat</h2> | |
| <p class="text-gray-600 mt-2"> | |
| Sign in to start chatting with our AI assistant | |
| </p> | |
| </div> | |
| <button | |
| id="signInBtn" | |
| class="btn btn-primary w-full gradient-bg border-0 text-white hover:opacity-90 transition-opacity" | |
| > | |
| <span class="material-icons mr-2">person_outline</span> Continue | |
| as Guest | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Chat Interface --> | |
| <div id="chatSection" style="display: none" class="flex gap-4 h-full"> | |
| <div class="flex-1 bg-white rounded-lg shadow-lg flex flex-col"> | |
| <div class="border-b p-4"> | |
| <div class="flex items-center justify-between"> | |
| <div class="flex items-center space-x-3"> | |
| <div | |
| class="w-10 h-10 rounded-full gradient-bg flex items-center justify-center" | |
| > | |
| <span class="material-icons text-white">smart_toy</span> | |
| </div> | |
| <div> | |
| <h3 id="selectedModelName" class="font-semibold"> | |
| AI Assistant | |
| </h3> | |
| <p id="selectedModelRole" class="text-sm text-gray-500"> | |
| Powered by Llama 3.2 | |
| </p> | |
| </div> | |
| </div> | |
| <div class="flex items-center gap-2 dropdown dropdown-end"> | |
| <select | |
| id="presetSelect" | |
| class="select select-bordered select-sm min-w-[180px]" | |
| > | |
| <option value="">No Preset</option> | |
| </select> | |
| <select | |
| id="modelSelect" | |
| class="select select-bordered select-sm min-w-[180px]" | |
| onchange="selectModel(this.value)" | |
| > | |
| <option value="">Default (Auto-route)</option> | |
| </select> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Model Quick Switcher Bar --> | |
| <div | |
| id="modelSwitcherBar" | |
| class="border-b px-4 py-2 bg-gray-50 hidden" | |
| > | |
| <div class="flex items-center gap-2 overflow-x-auto"> | |
| <span class="text-xs text-gray-500 whitespace-nowrap" | |
| >Quick switch:</span | |
| > | |
| <div id="modelQuickSwitcher" class="flex gap-2"></div> | |
| </div> | |
| </div> | |
| <!-- Messages Area --> | |
| <div | |
| class="message-area flex-1 p-4 space-y-4" | |
| id="messageArea" | |
| ></div> | |
| <!-- Input Area --> | |
| <div class="border-t p-4"> | |
| <!-- Chat Mode Tabs --> | |
| <div class="tabs tabs-boxed mb-3 bg-gray-100"> | |
| <button | |
| id="modeChat" | |
| class="tab tab-active" | |
| onclick="setChatMode('chat')" | |
| > | |
| <span class="material-icons text-sm mr-1">chat</span>Chat | |
| </button> | |
| <button | |
| id="modeTextGen" | |
| class="tab" | |
| onclick="setChatMode('text-generation')" | |
| > | |
| <span class="material-icons text-sm mr-1">text_fields</span | |
| >Text | |
| </button> | |
| <button | |
| id="modeImage" | |
| class="tab" | |
| onclick="setChatMode('image-generation')" | |
| > | |
| <span class="material-icons text-sm mr-1">image</span>Image | |
| </button> | |
| <button | |
| id="modeTranslate" | |
| class="tab" | |
| onclick="setChatMode('translation')" | |
| > | |
| <span class="material-icons text-sm mr-1">translate</span | |
| >Translate | |
| </button> | |
| <button | |
| id="modeQA" | |
| class="tab" | |
| onclick="setChatMode('question-answering')" | |
| > | |
| <span class="material-icons text-sm mr-1">quiz</span>Q&A | |
| </button> | |
| <button | |
| id="modeSummary" | |
| class="tab" | |
| onclick="setChatMode('summarization')" | |
| > | |
| <span class="material-icons text-sm mr-1">summarize</span | |
| >Summary | |
| </button> | |
| </div> | |
| <!-- Context Input (for QA mode) --> | |
| <div id="contextInputArea" class="hidden mb-3"> | |
| <label class="label" | |
| ><span class="label-text text-xs" | |
| >Context (for Q&A)</span | |
| ></label | |
| > | |
| <textarea | |
| id="contextInput" | |
| class="textarea textarea-bordered w-full h-20 text-sm" | |
| placeholder="Paste the context text here for question answering..." | |
| ></textarea> | |
| </div> | |
| <!-- Translation Options --> | |
| <div id="translationOptions" class="hidden mb-3"> | |
| <div class="flex gap-2"> | |
| <select | |
| id="translateModel" | |
| class="select select-bordered select-sm flex-1" | |
| > | |
| <option value="Helsinki-NLP/opus-mt-en-zh"> | |
| English → Chinese | |
| </option> | |
| <option value="Helsinki-NLP/opus-mt-en-ja"> | |
| English → Japanese | |
| </option> | |
| <option value="Helsinki-NLP/opus-mt-en-ko"> | |
| English → Korean | |
| </option> | |
| <option value="Helsinki-NLP/opus-mt-en-fr"> | |
| English → French | |
| </option> | |
| <option value="Helsinki-NLP/opus-mt-en-de"> | |
| English → German | |
| </option> | |
| <option value="Helsinki-NLP/opus-mt-en-es"> | |
| English → Spanish | |
| </option> | |
| <option value="Helsinki-NLP/opus-mt-zh-en"> | |
| Chinese → English | |
| </option> | |
| <option value="Helsinki-NLP/opus-mt-ja-en"> | |
| Japanese → English | |
| </option> | |
| </select> | |
| </div> | |
| </div> | |
| <!-- Image Generation Options --> | |
| <div id="imageOptions" class="hidden mb-3"> | |
| <div class="flex gap-2"> | |
| <input | |
| id="negativePrompt" | |
| type="text" | |
| class="input input-bordered input-sm flex-1" | |
| placeholder="Negative prompt (what to avoid)..." | |
| /> | |
| <select | |
| id="imageModel" | |
| class="select select-bordered select-sm" | |
| > | |
| <option value="stabilityai/stable-diffusion-xl-base-1.0"> | |
| SDXL Base | |
| </option> | |
| <option value="runwayml/stable-diffusion-v1-5"> | |
| SD v1.5 | |
| </option> | |
| <option value="CompVis/stable-diffusion-v1-4"> | |
| SD v1.4 | |
| </option> | |
| </select> | |
| </div> | |
| </div> | |
| <div class="flex space-x-2"> | |
| <input | |
| id="messageInput" | |
| type="text" | |
| placeholder="Type your message..." | |
| class="flex-1 input input-bordered focus:outline-none focus:ring-2 focus:ring-purple-500" | |
| /> | |
| <button | |
| id="sendBtn" | |
| class="btn btn-primary gradient-bg border-0 text-white hover:opacity-90 transition-opacity" | |
| > | |
| <span class="material-icons">send</span> | |
| </button> | |
| </div> | |
| <div class="flex justify-center mt-2 space-x-2 flex-wrap"> | |
| <button id="quickReply1" class="btn btn-xs btn-outline"> | |
| 👋 Hello | |
| </button> | |
| <button id="quickReply2" class="btn btn-xs btn-outline"> | |
| 😄 Joke | |
| </button> | |
| <button id="quickReply3" class="btn btn-xs btn-outline"> | |
| 💻 Coding | |
| </button> | |
| <button id="quickReply4" class="btn btn-xs btn-outline"> | |
| 🤖 AI | |
| </button> | |
| <button id="quickReply5" class="btn btn-xs btn-outline"> | |
| 📚 Story | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Sidebar --> | |
| <div class="w-80 bg-white rounded-lg shadow-lg p-4 hidden lg:block"> | |
| <h3 class="font-semibold mb-4 flex items-center"> | |
| <span class="material-icons mr-2">history</span> Chat History | |
| </h3> | |
| <div | |
| id="chatHistory" | |
| class="space-y-2 max-h-48 overflow-y-auto" | |
| ></div> | |
| <div class="mt-6 pt-6 border-t"> | |
| <h3 class="font-semibold mb-3 flex items-center"> | |
| <span class="material-icons mr-2">psychology</span> Active | |
| Models | |
| </h3> | |
| <div | |
| id="activeModels" | |
| class="space-y-2 max-h-48 overflow-y-auto" | |
| ></div> | |
| <button | |
| id="manageModelsBtn" | |
| class="btn btn-sm btn-outline w-full mt-3" | |
| > | |
| <span class="material-icons text-sm">add</span> Manage Models | |
| </button> | |
| <button | |
| id="managePresetsBtn" | |
| class="btn btn-sm btn-outline w-full mt-2" | |
| > | |
| <span class="material-icons text-sm">person_pin</span> Manage Presets | |
| </button> | |
| </div> | |
| <div class="mt-6 pt-6 border-t"> | |
| <h3 class="font-semibold mb-3 flex items-center"> | |
| <span class="material-icons mr-2">share</span> Share Chat | |
| </h3> | |
| <button id="shareBtn" class="btn btn-sm btn-outline w-full"> | |
| <span class="material-icons text-sm">share</span> Share | |
| Conversation | |
| </button> | |
| </div> | |
| <div class="mt-4"> | |
| <h3 class="font-semibold mb-3 flex items-center"> | |
| <span class="material-icons mr-2">settings</span> Global | |
| Settings | |
| </h3> | |
| <label class="label" | |
| ><span class="label-text">API Key</span></label | |
| > | |
| <input | |
| id="apiKey" | |
| type="password" | |
| class="input input-bordered input-sm w-full mb-2" | |
| placeholder="HuggingFace API Key" | |
| value="" | |
| /> | |
| <label class="label" | |
| ><span class="label-text">Temperature</span></label | |
| > | |
| <select | |
| id="globalTemp" | |
| class="select select-bordered select-sm w-full" | |
| > | |
| <option value="0.1">Precise (0.1)</option> | |
| <option value="0.3" selected>Balanced (0.3)</option> | |
| <option value="0.5">Creative (0.5)</option> | |
| <option value="0.7">Very Creative (0.7)</option> | |
| </select> | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| </div> | |
| </div> | |
| <script> | |
| // Default Firebase config (will be overridden by settings) | |
| let firebaseConfig = { | |
| apiKey: "AIzaSyCvkk4P_PPidbmxql863MJI0VDt4GdGdPk", | |
| authDomain: "ai-assistant-fbfb7.firebaseapp.com", | |
| projectId: "ai-assistant-fbfb7", | |
| storageBucket: "ai-assistant-fbfb7.firebasestorage.app", | |
| messagingSenderId: "19639341578", | |
| appId: "1:19639341578:web:88545e8fe038656a1745f3", | |
| measurementId: "G-6F1GP7V42N", | |
| }; | |
| // Settings storage | |
| let appSettings = {}; | |
| let modelSettings = { models: [] }; | |
| // Initialize Firebase (will be reinitialized after loading settings) | |
| firebase.initializeApp(firebaseConfig); | |
| let auth = firebase.auth(); | |
| let database = firebase.database(); | |
| const app = { | |
| user: null, | |
| messages: [], | |
| models: [], | |
| chatSessions: [], | |
| currentSessionId: null, | |
| selectedModelId: "", | |
| editingModelId: null, | |
| isTyping: false, | |
| chatMode: "chat", // chat, text-generation, image-generation, translation, question-answering, summarization | |
| // Preset/Persona state | |
| presets: [], | |
| userPresets: [], | |
| curatedPresets: [], | |
| activePresetId: "", | |
| editingPresetId: null, | |
| presetKeywords: [], | |
| presetDatasets: [], | |
| }; | |
| // Curated presets - available offline | |
| const CURATED_PRESETS = [ | |
| { | |
| id: "curated-helper", | |
| name: "Helpful Assistant", | |
| icon: "🤖", | |
| modelId: "meta-llama/Llama-3.2-3B-Instruct", | |
| systemPrompt: "You are a helpful, friendly, and knowledgeable AI assistant. You provide clear, accurate, and concise answers to questions. You are polite and respectful.", | |
| temperature: 0.3, | |
| maxTokens: 500, | |
| keywords: ["help", "assistant", "general", "question", "answer"], | |
| isCustom: false, | |
| description: "General purpose helper for everyday tasks" | |
| }, | |
| { | |
| id: "curated-coder", | |
| name: "Code Expert", | |
| icon: "💻", | |
| modelId: "meta-llama/Llama-3.2-3B-Instruct", | |
| systemPrompt: "You are an expert programmer with deep knowledge of multiple programming languages, algorithms, and best practices. You provide clean, well-documented code with explanations.", | |
| temperature: 0.2, | |
| maxTokens: 800, | |
| keywords: ["code", "programming", "bug", "debug", "function", "class", "algorithm", "software"], | |
| isCustom: false, | |
| description: "Expert programming help and code review" | |
| }, | |
| { | |
| id: "curated-creative", | |
| name: "Creative Writer", | |
| icon: "✨", | |
| modelId: "NousResearch/Hermes-3-Llama-3.1-8B", | |
| systemPrompt: "You are a creative writer with a vivid imagination and a flair for storytelling. You help users craft engaging narratives, poems, stories, and creative content.", | |
| temperature: 0.8, | |
| maxTokens: 1000, | |
| keywords: ["story", "creative", "write", "poem", "narrative", "imagine", "fiction"], | |
| isCustom: false, | |
| description: "Creative writing and storytelling assistance" | |
| }, | |
| { | |
| id: "curated-teacher", | |
| name: "Learning Tutor", | |
| icon: "📚", | |
| modelId: "meta-llama/Llama-3.2-3B-Instruct", | |
| systemPrompt: "You are a patient and encouraging teacher who explains complex concepts clearly. You use analogies, examples, and step-by-step explanations to help learners understand.", | |
| temperature: 0.3, | |
| maxTokens: 600, | |
| keywords: ["teach", "learn", "explain", "concept", "understand", "tutorial", "education"], | |
| isCustom: false, | |
| description: "Patient educational assistance and tutoring" | |
| }, | |
| { | |
| id: "curated-analyst", | |
| name: "Data Analyst", | |
| icon: "📊", | |
| modelId: "meta-llama/Llama-3.2-3B-Instruct", | |
| systemPrompt: "You are a data analyst who helps interpret data, identify patterns, and provide insights. You are thorough, analytical, and data-driven in your approach.", | |
| temperature: 0.2, | |
| maxTokens: 700, | |
| keywords: ["data", "analyze", "statistics", "insight", "pattern", "report", "metrics"], | |
| isCustom: false, | |
| description: "Data analysis and interpretation" | |
| } | |
| ]; | |
| // Toast notification system | |
| function showToast(message, type = "info", duration = 3000) { | |
| // Remove existing toasts | |
| const existingContainer = document.getElementById("toastContainer"); | |
| if (existingContainer) { | |
| existingContainer.remove(); | |
| } | |
| // Create toast container | |
| const container = document.createElement("div"); | |
| container.id = "toastContainer"; | |
| container.className = "fixed top-4 right-4 z-50 flex flex-col gap-2"; | |
| // Create toast element | |
| const toast = document.createElement("div"); | |
| const typeClasses = { | |
| success: "bg-success text-white", | |
| error: "bg-error text-white", | |
| info: "bg-info text-white", | |
| warning: "bg-warning text-black" | |
| }; | |
| toast.className = `alert ${typeClasses[type] || typeClasses.info} shadow-lg max-w-md`; | |
| toast.innerHTML = `<span>${message}</span>`; | |
| container.appendChild(toast); | |
| document.body.appendChild(container); | |
| // Auto dismiss | |
| setTimeout(() => { | |
| toast.remove(); | |
| if (container.children.length === 0) { | |
| container.remove(); | |
| } | |
| }, duration); | |
| } | |
| document.addEventListener("DOMContentLoaded", async () => { | |
| await loadAllSettings(); | |
| setupEventListeners(); | |
| setupAuthListener(); | |
| loadApiKey(); | |
| }); | |
| async function loadAllSettings() { | |
| try { | |
| // Load model settings | |
| const modelResponse = await fetch("/api/settings/models"); | |
| if (modelResponse.ok) { | |
| modelSettings = await modelResponse.json(); | |
| console.log("Model settings loaded:", modelSettings); | |
| } | |
| // Load app settings | |
| const appResponse = await fetch("/api/settings/app"); | |
| if (appResponse.ok) { | |
| appSettings = await appResponse.json(); | |
| console.log("App settings loaded:", appSettings); | |
| } | |
| } catch (error) { | |
| console.error("Error loading settings:", error); | |
| } | |
| } | |
| function setupEventListeners() { | |
| document | |
| .getElementById("signInBtn") | |
| .addEventListener("click", signInAnonymously); | |
| document.getElementById("logoutBtn").addEventListener("click", logout); | |
| document | |
| .getElementById("statsIcon") | |
| .addEventListener("click", () => toggleModal("statsModal")); | |
| document | |
| .getElementById("mapIcon") | |
| .addEventListener("click", () => toggleModal("mapModal")); | |
| document | |
| .getElementById("modelsBtn") | |
| .addEventListener("click", () => toggleModal("modelsModal")); | |
| document | |
| .getElementById("closeStatsBtn") | |
| .addEventListener("click", () => toggleModal("statsModal")); | |
| document | |
| .getElementById("closeMapBtn") | |
| .addEventListener("click", () => toggleModal("mapModal")); | |
| document | |
| .getElementById("closeModelsBtn") | |
| .addEventListener("click", () => toggleModal("modelsModal")); | |
| document | |
| .getElementById("manageModelsBtn") | |
| .addEventListener("click", () => toggleModal("modelsModal")); | |
| document | |
| .getElementById("sendBtn") | |
| .addEventListener("click", sendMessage); | |
| document | |
| .getElementById("messageInput") | |
| .addEventListener("keyup", (e) => { | |
| if (e.key === "Enter") sendMessage(); | |
| }); | |
| document | |
| .getElementById("shareBtn") | |
| .addEventListener("click", shareChat); | |
| document | |
| .getElementById("quickReply1") | |
| .addEventListener("click", () => quickReply("Hello!")); | |
| document | |
| .getElementById("quickReply2") | |
| .addEventListener("click", () => quickReply("Tell me a joke")); | |
| document | |
| .getElementById("quickReply3") | |
| .addEventListener("click", () => quickReply("Help me with coding")); | |
| document | |
| .getElementById("quickReply4") | |
| .addEventListener("click", () => quickReply("Explain AI")); | |
| document | |
| .getElementById("quickReply5") | |
| .addEventListener("click", () => quickReply("Write a story")); | |
| document | |
| .getElementById("saveModelBtn") | |
| .addEventListener("click", saveModel); | |
| document | |
| .getElementById("cancelEditBtn") | |
| .addEventListener("click", cancelEditModel); | |
| document.getElementById("modelTemp").addEventListener("input", (e) => { | |
| document.getElementById("tempValue").textContent = e.target.value; | |
| }); | |
| document.getElementById("apiKey").addEventListener("change", (e) => { | |
| localStorage.setItem("huggingface_api_key", e.target.value); | |
| }); | |
| // Model info and keywords event listeners | |
| document | |
| .getElementById("fetchModelInfoBtn") | |
| .addEventListener("click", fetchModelInfo); | |
| document | |
| .getElementById("addKeywordBtn") | |
| .addEventListener("click", addCustomKeyword); | |
| document | |
| .getElementById("customKeyword") | |
| .addEventListener("keyup", (e) => { | |
| if (e.key === "Enter") addCustomKeyword(); | |
| }); | |
| // Preset-related event listeners | |
| const presetSelect = document.getElementById("presetSelect"); | |
| if (presetSelect) { | |
| presetSelect.addEventListener("change", (e) => { | |
| const presetId = e.target.value; | |
| if (presetId) { | |
| selectPreset(presetId); | |
| } else { | |
| clearPreset(); | |
| } | |
| }); | |
| } | |
| const managePresetsBtn = document.getElementById("managePresetsBtn"); | |
| if (managePresetsBtn) { | |
| managePresetsBtn.addEventListener("click", () => toggleModal("presetManagerModal")); | |
| } | |
| const closePresetManagerBtn = document.getElementById("closePresetManagerBtn"); | |
| if (closePresetManagerBtn) { | |
| closePresetManagerBtn.addEventListener("click", () => toggleModal("presetManagerModal")); | |
| } | |
| const closePresetsModalBtn = document.getElementById("closePresetsModalBtn"); | |
| if (closePresetsModalBtn) { | |
| closePresetsModalBtn.addEventListener("click", () => toggleModal("presetsModal")); | |
| } | |
| const presetSaveBtn = document.getElementById("presetSaveBtn"); | |
| if (presetSaveBtn) { | |
| presetSaveBtn.addEventListener("click", savePreset); | |
| } | |
| const presetCancelBtn = document.getElementById("presetCancelBtn"); | |
| if (presetCancelBtn) { | |
| presetCancelBtn.addEventListener("click", () => { | |
| resetPresetForm(); | |
| toggleModal("presetsModal"); | |
| }); | |
| } | |
| const presetTempInput = document.getElementById("presetTemperature"); | |
| if (presetTempInput) { | |
| presetTempInput.addEventListener("input", (e) => { | |
| document.getElementById("presetTempValue").textContent = e.target.value; | |
| }); | |
| } | |
| const presetKeywordBtn = document.getElementById("addPresetKeywordBtn"); | |
| if (presetKeywordBtn) { | |
| presetKeywordBtn.addEventListener("click", addPresetKeyword); | |
| } | |
| const presetKeywordInput = document.getElementById("presetKeywordInput"); | |
| if (presetKeywordInput) { | |
| presetKeywordInput.addEventListener("keyup", (e) => { | |
| if (e.key === "Enter") addPresetKeyword(); | |
| }); | |
| } | |
| const presetDatasetBtn = document.getElementById("addPresetDatasetBtn"); | |
| if (presetDatasetBtn) { | |
| presetDatasetBtn.addEventListener("click", addPresetDataset); | |
| } | |
| const newPresetBtn = document.getElementById("newPresetBtn"); | |
| if (newPresetBtn) { | |
| newPresetBtn.addEventListener("click", () => { | |
| resetPresetForm(); | |
| toggleModal("presetsModal"); | |
| }); | |
| } | |
| } | |
| // Keywords state | |
| let modelKeywords = []; | |
| async function fetchModelInfo() { | |
| const modelId = document.getElementById("modelId").value.trim(); | |
| const statusEl = document.getElementById("modelInfoStatus"); | |
| const infoPanel = document.getElementById("modelInfoPanel"); | |
| const infoContent = document.getElementById("modelInfoContent"); | |
| if (!modelId) { | |
| statusEl.innerHTML = | |
| '<span class="text-red-500">Please enter a model ID</span>'; | |
| return; | |
| } | |
| statusEl.innerHTML = | |
| '<span class="text-blue-500"><span class="loading loading-spinner loading-xs"></span> Fetching...</span>'; | |
| try { | |
| const response = await fetch( | |
| `/api/model-info?model_id=${encodeURIComponent(modelId)}`, | |
| ); | |
| const data = await response.json(); | |
| if (data.error) { | |
| throw new Error(data.error); | |
| } | |
| // Update keywords | |
| modelKeywords = data.keywords || []; | |
| renderKeywords(); | |
| // Update model name if empty | |
| const nameInput = document.getElementById("modelName"); | |
| if (!nameInput.value.trim()) { | |
| nameInput.value = modelId.split("/").pop(); | |
| } | |
| // Show model info panel | |
| infoPanel.classList.remove("hidden"); | |
| infoContent.innerHTML = ` | |
| <div class="bg-white rounded p-2"> | |
| <span class="text-gray-500 text-xs">Author</span> | |
| <p class="font-medium">${data.author || "Unknown"}</p> | |
| </div> | |
| <div class="bg-white rounded p-2"> | |
| <span class="text-gray-500 text-xs">Task</span> | |
| <p class="font-medium">${data.pipeline_tag || "N/A"}</p> | |
| </div> | |
| <div class="bg-white rounded p-2"> | |
| <span class="text-gray-500 text-xs">Downloads</span> | |
| <p class="font-medium">${formatNumber(data.downloads)}</p> | |
| </div> | |
| <div class="bg-white rounded p-2"> | |
| <span class="text-gray-500 text-xs">Likes</span> | |
| <p class="font-medium">❤️ ${formatNumber(data.likes)}</p> | |
| </div> | |
| `; | |
| statusEl.innerHTML = | |
| '<span class="text-green-500">✓ Model info loaded</span>'; | |
| } catch (error) { | |
| console.error("Error fetching model info:", error); | |
| statusEl.innerHTML = `<span class="text-red-500">Error: ${error.message}</span>`; | |
| infoPanel.classList.add("hidden"); | |
| } | |
| } | |
| function renderKeywords() { | |
| const container = document.getElementById("keywordsContainer"); | |
| if (modelKeywords.length === 0) { | |
| container.innerHTML = | |
| '<span class="text-gray-400 text-sm">No keywords. Click "Fetch" or add custom keywords.</span>'; | |
| return; | |
| } | |
| container.innerHTML = modelKeywords | |
| .map( | |
| (keyword, index) => ` | |
| <span class="keyword-tag"> | |
| ${keyword} | |
| <button onclick="removeKeyword(${index})" class="ml-1 hover:text-red-500">×</button> | |
| </span> | |
| `, | |
| ) | |
| .join(""); | |
| } | |
| function removeKeyword(index) { | |
| modelKeywords.splice(index, 1); | |
| renderKeywords(); | |
| } | |
| function addCustomKeyword() { | |
| const input = document.getElementById("customKeyword"); | |
| const keyword = input.value.trim().toLowerCase(); | |
| if (keyword && !modelKeywords.includes(keyword)) { | |
| modelKeywords.push(keyword); | |
| renderKeywords(); | |
| input.value = ""; | |
| } | |
| } | |
| function formatNumber(num) { | |
| if (num >= 1000000) return (num / 1000000).toFixed(1) + "M"; | |
| if (num >= 1000) return (num / 1000).toFixed(1) + "K"; | |
| return num.toString(); | |
| } | |
| function setupAuthListener() { | |
| auth.onAuthStateChanged((user) => { | |
| if (user) { | |
| app.user = user; | |
| console.log("User authenticated:", user.uid); | |
| showChatInterface(); | |
| loadModels(); | |
| loadPresets(); | |
| setupPresetFirebaseListeners(); | |
| loadChatSessions(); | |
| startNewSession(); | |
| } else { | |
| hideChatInterface(); | |
| } | |
| }); | |
| } | |
| function toggleModal(modalId) { | |
| const modal = document.getElementById(modalId); | |
| modal.style.display = modal.style.display === "none" ? "flex" : "none"; | |
| } | |
| function showChatInterface() { | |
| document.getElementById("authSection").style.display = "none"; | |
| document.getElementById("chatSection").style.display = "flex"; | |
| document.getElementById("modelsBtn").style.display = "block"; | |
| document.getElementById("logoutBtn").style.display = "block"; | |
| } | |
| function hideChatInterface() { | |
| document.getElementById("authSection").style.display = "block"; | |
| document.getElementById("chatSection").style.display = "none"; | |
| document.getElementById("modelsBtn").style.display = "none"; | |
| document.getElementById("logoutBtn").style.display = "none"; | |
| } | |
| async function signInAnonymously() { | |
| try { | |
| const result = await auth.signInAnonymously(); | |
| console.log("Anonymous sign-in success:", result.user.uid); | |
| } catch (error) { | |
| console.error("Error signing in anonymously:", error); | |
| } | |
| } | |
| function logout() { | |
| auth | |
| .signOut() | |
| .catch((error) => console.error("Error signing out:", error)); | |
| } | |
| async function loadModels() { | |
| try { | |
| // Combine models from settings file and Firebase | |
| let allModels = []; | |
| // Load from settings file | |
| if (modelSettings.models && modelSettings.models.length > 0) { | |
| allModels = [...modelSettings.models]; | |
| } | |
| // Load from Firebase if user is authenticated | |
| if (app.user) { | |
| try { | |
| const snapshot = await database | |
| .ref(`users/${app.user.uid}/models`) | |
| .once("value"); | |
| const firebaseData = snapshot.val() || {}; | |
| const firebaseModels = Object.entries(firebaseData).map( | |
| ([id, model]) => ({ | |
| id, | |
| ...model, | |
| }), | |
| ); | |
| // Merge Firebase models (avoid duplicates by ID) | |
| firebaseModels.forEach((fbModel) => { | |
| const existingIndex = allModels.findIndex( | |
| (m) => m.id === fbModel.id, | |
| ); | |
| if (existingIndex >= 0) { | |
| // Update existing model with Firebase data if newer | |
| if (fbModel.updatedAt > allModels[existingIndex].updatedAt) { | |
| allModels[existingIndex] = fbModel; | |
| } | |
| } else { | |
| allModels.push(fbModel); | |
| } | |
| }); | |
| } catch (fbError) { | |
| console.log("Firebase models not available:", fbError.message); | |
| } | |
| } | |
| app.models = allModels; | |
| renderModels(); | |
| renderModelSelect(); | |
| } catch (error) { | |
| console.error("Error loading models:", error); | |
| } | |
| } | |
| function renderModels() { | |
| const modelsList = document.getElementById("modelsList"); | |
| if (app.models.length === 0) { | |
| modelsList.innerHTML = | |
| '<div class="text-center py-12 bg-gray-50 rounded-lg"><span class="material-icons text-6xl text-gray-300">psychology</span><p class="text-gray-500 mt-4">No models configured yet. Add your first AI model above!</p></div>'; | |
| return; | |
| } | |
| modelsList.innerHTML = `<h3 class="font-semibold text-lg mb-4">Your Models (${app.models.length})</h3><div class="grid grid-cols-1 md:grid-cols-2 gap-4">${app.models.map((model) => `<div class="model-card bg-white border rounded-lg p-4 ${model.enabled ? "border-green-400 bg-green-50" : "border-gray-200"}"><div class="flex justify-between items-start mb-2"><div><h4 class="font-semibold flex items-center">${model.name}<span class="badge badge-sm ml-2 ${model.enabled ? "badge-success" : "badge-ghost"}">${model.enabled ? "Active" : "Disabled"}</span></h4><p class="text-sm text-gray-500">${model.role} • ${model.modelId.split("/").pop()}</p></div><div class="flex gap-1"><button class="btn btn-sm btn-ghost btn-circle" onclick="editModel('${model.id}')"><span class="material-icons text-sm">edit</span></button><button class="btn btn-sm btn-ghost btn-circle text-red-500" onclick="deleteModel('${model.id}')"><span class="material-icons text-sm">delete</span></button></div></div><p class="text-xs text-gray-400 truncate">${model.systemPrompt || "No system prompt"}</p></div>`).join("")}</div>`; | |
| } | |
| function renderModelSelect() { | |
| const select = document.getElementById("modelSelect"); | |
| const enabledModels = app.models.filter((m) => m.enabled); | |
| // Get default model from settings | |
| const defaultModelId = | |
| modelSettings.defaultModel || "meta-llama/Llama-3.2-3B-Instruct"; | |
| select.innerHTML = | |
| `<option value="">Default (${defaultModelId.split("/").pop()})</option>` + | |
| enabledModels | |
| .map( | |
| (model) => | |
| `<option value="${model.id}" ${app.selectedModelId === model.id ? "selected" : ""}>${model.name} (${model.role})</option>`, | |
| ) | |
| .join(""); | |
| // Render quick switcher bar | |
| renderQuickSwitcher(enabledModels); | |
| // Also render active models in sidebar | |
| renderActiveModels(); | |
| // Update header with selected model info | |
| updateSelectedModelDisplay(); | |
| } | |
| function renderQuickSwitcher(models) { | |
| const bar = document.getElementById("modelSwitcherBar"); | |
| const container = document.getElementById("modelQuickSwitcher"); | |
| if (!models || models.length === 0) { | |
| bar.classList.add("hidden"); | |
| return; | |
| } | |
| bar.classList.remove("hidden"); | |
| container.innerHTML = models | |
| .slice(0, 5) | |
| .map( | |
| (model) => ` | |
| <button | |
| onclick="selectModel('${model.id}')" | |
| class="btn btn-xs ${app.selectedModelId === model.id ? "btn-primary" : "btn-ghost"} whitespace-nowrap" | |
| > | |
| <span class="material-icons text-xs mr-1">${getRoleIcon(model.role)}</span> | |
| ${model.name} | |
| </button> | |
| `, | |
| ) | |
| .join(""); | |
| } | |
| function renderActiveModels() { | |
| const container = document.getElementById("activeModels"); | |
| const enabledModels = app.models.filter((m) => m.enabled); | |
| if (enabledModels.length === 0) { | |
| container.innerHTML = | |
| '<p class="text-sm text-gray-400">No active models</p>'; | |
| return; | |
| } | |
| container.innerHTML = enabledModels | |
| .map( | |
| (model) => ` | |
| <div class="p-2 rounded-lg cursor-pointer transition-all hover:bg-purple-50 ${app.selectedModelId === model.id ? "bg-purple-100 border-2 border-purple-400" : "bg-gray-50 border border-gray-200"}" | |
| onclick="selectModel('${model.id}')"> | |
| <div class="flex items-center justify-between"> | |
| <div class="flex items-center space-x-2"> | |
| <div class="w-8 h-8 rounded-full ${app.selectedModelId === model.id ? "gradient-bg" : "bg-gray-300"} flex items-center justify-center"> | |
| <span class="material-icons text-white text-sm">${getRoleIcon(model.role)}</span> | |
| </div> | |
| <div> | |
| <p class="text-sm font-medium">${model.name}</p> | |
| <p class="text-xs text-gray-500">${model.role}</p> | |
| </div> | |
| </div> | |
| ${app.selectedModelId === model.id ? '<span class="material-icons text-purple-500 text-sm">check_circle</span>' : ""} | |
| </div> | |
| </div> | |
| `, | |
| ) | |
| .join(""); | |
| } | |
| function getRoleIcon(role) { | |
| const icons = { | |
| assistant: "smart_toy", | |
| expert: "school", | |
| coder: "code", | |
| analyst: "analytics", | |
| teacher: "menu_book", | |
| creative: "palette", | |
| custom: "settings", | |
| }; | |
| return icons[role] || "smart_toy"; | |
| } | |
| // Auto-routing function - find best matching model/preset based on keywords | |
| function findMatchingModel(message, presetId = null) { | |
| const messageLower = message.toLowerCase(); | |
| const words = messageLower.split(/\s+/).filter(w => w.length > 2); | |
| // If a preset is specified, check its keywords first | |
| if (presetId) { | |
| const preset = app.presets.find(p => p.id === presetId); | |
| if (preset && preset.keywords) { | |
| const presetMatches = preset.keywords.filter(kw => | |
| messageLower.includes(kw.toLowerCase()) | |
| ); | |
| if (presetMatches.length > 0) { | |
| return preset.modelId; | |
| } | |
| } | |
| } | |
| // Fall back to checking model keywords | |
| let bestMatch = null; | |
| let bestScore = 0; | |
| for (const model of app.models.filter(m => m.enabled)) { | |
| if (!model.keywords) continue; | |
| let score = 0; | |
| for (const keyword of model.keywords) { | |
| const kwLower = keyword.toLowerCase(); | |
| if (messageLower.includes(kwLower)) { | |
| score += 1; | |
| // Boost score for exact word matches | |
| if (words.includes(kwLower)) { | |
| score += 1; | |
| } | |
| } | |
| } | |
| if (score > bestScore) { | |
| bestScore = score; | |
| bestMatch = model.modelId; | |
| } | |
| } | |
| return bestMatch || (modelSettings.defaultModel || "meta-llama/Llama-3.2-3B-Instruct"); | |
| } | |
| // ============ Preset/Persona Functions ============ | |
| // Load presets from Firebase and merge with curated presets | |
| async function loadPresets() { | |
| try { | |
| app.curatedPresets = [...CURATED_PRESETS]; | |
| app.userPresets = []; | |
| if (app.user) { | |
| try { | |
| const snapshot = await database | |
| .ref(`users/${app.user.uid}/presets`) | |
| .once("value"); | |
| const firebaseData = snapshot.val() || {}; | |
| app.userPresets = Object.entries(firebaseData).map( | |
| ([id, preset]) => ({ | |
| id, | |
| ...preset, | |
| isCustom: true | |
| }) | |
| ); | |
| } catch (fbError) { | |
| console.log("Firebase presets not available:", fbError.message); | |
| } | |
| } | |
| // Load active preset from settings | |
| if (app.user) { | |
| try { | |
| const activeSnapshot = await database | |
| .ref(`users/${app.user.uid}/settings/activePresetId`) | |
| .once("value"); | |
| app.activePresetId = activeSnapshot.val() || ""; | |
| } catch (e) { | |
| console.log("Could not load active preset:", e); | |
| } | |
| } | |
| updatePresetsList(); | |
| } catch (error) { | |
| console.error("Error loading presets:", error); | |
| } | |
| } | |
| // Set up Firebase real-time listeners for presets | |
| function setupPresetFirebaseListeners() { | |
| if (!app.user) return; | |
| const presetsRef = database.ref(`users/${app.user.uid}/presets`); | |
| presetsRef.on("child_added", (snapshot) => { | |
| const preset = { id: snapshot.key, ...snapshot.val(), isCustom: true }; | |
| const existingIndex = app.userPresets.findIndex(p => p.id === preset.id); | |
| if (existingIndex < 0) { | |
| app.userPresets.push(preset); | |
| updatePresetsList(); | |
| } | |
| }); | |
| presetsRef.on("child_changed", (snapshot) => { | |
| const preset = { id: snapshot.key, ...snapshot.val(), isCustom: true }; | |
| const index = app.userPresets.findIndex(p => p.id === preset.id); | |
| if (index >= 0) { | |
| app.userPresets[index] = preset; | |
| updatePresetsList(); | |
| } | |
| }); | |
| presetsRef.on("child_removed", (snapshot) => { | |
| app.userPresets = app.userPresets.filter(p => p.id !== snapshot.key); | |
| updatePresetsList(); | |
| }); | |
| } | |
| // Update combined presets list and re-render | |
| function updatePresetsList() { | |
| app.presets = [...app.curatedPresets, ...app.userPresets]; | |
| renderPresetSelector(); | |
| renderPresetManager(); | |
| // Apply active preset if set | |
| if (app.activePresetId) { | |
| applyPreset(app.activePresetId); | |
| } | |
| } | |
| // Select a preset and save to Firebase | |
| async function selectPreset(presetId) { | |
| app.activePresetId = presetId; | |
| if (app.user) { | |
| await database | |
| .ref(`users/${app.user.uid}/settings/activePresetId`) | |
| .set(presetId); | |
| } | |
| // Save to current session | |
| if (app.currentSessionId) { | |
| await database | |
| .ref(`chats/${app.user.uid}/${app.currentSessionId}/presetId`) | |
| .set(presetId); | |
| } | |
| applyPreset(presetId); | |
| showToast("Preset applied successfully", "success"); | |
| } | |
| // Apply preset settings to current session | |
| function applyPreset(presetId) { | |
| const preset = app.presets.find(p => p.id === presetId); | |
| if (!preset) return; | |
| // Update header display | |
| document.getElementById("selectedModelName").textContent = preset.name; | |
| document.getElementById("selectedModelRole").textContent = preset.description || preset.modelId.split("/").pop(); | |
| // Update model select dropdown | |
| const modelSelect = document.getElementById("modelSelect"); | |
| modelSelect.value = presetId; | |
| console.log("Applied preset:", preset.name); | |
| } | |
| // Get the currently active preset | |
| function getActivePreset() { | |
| return app.presets.find(p => p.id === app.activePresetId) || null; | |
| } | |
| // Clear active preset | |
| async function clearPreset() { | |
| app.activePresetId = ""; | |
| if (app.user) { | |
| await database | |
| .ref(`users/${app.user.uid}/settings/activePresetId`) | |
| .set(""); | |
| } | |
| const modelSelect = document.getElementById("modelSelect"); | |
| modelSelect.value = ""; | |
| updateSelectedModelDisplay(); | |
| showToast("Preset cleared", "info"); | |
| } | |
| // Save a preset to Firebase | |
| async function savePreset() { | |
| const name = document.getElementById("presetName").value.trim(); | |
| const modelId = document.getElementById("presetModelId").value.trim(); | |
| const systemPrompt = document.getElementById("presetSystemPrompt").value.trim(); | |
| const description = document.getElementById("presetDescription").value.trim(); | |
| const icon = document.getElementById("presetIcon").value.trim() || "🎯"; | |
| const temperature = parseFloat(document.getElementById("presetTemperature").value); | |
| const maxTokens = parseInt(document.getElementById("presetMaxTokens").value); | |
| if (!name || !modelId || !systemPrompt) { | |
| showToast("Please fill in required fields", "error"); | |
| return; | |
| } | |
| const id = app.editingPresetId || `preset-${Date.now()}`; | |
| const presetData = { | |
| id, | |
| name, | |
| description, | |
| icon, | |
| modelId, | |
| systemPrompt, | |
| temperature, | |
| maxTokens, | |
| keywords: app.presetKeywords, | |
| datasets: app.presetDatasets, | |
| isCustom: true, | |
| createdAt: app.editingPresetId ? undefined : Date.now(), | |
| updatedAt: Date.now(), | |
| }; | |
| try { | |
| // Validate with backend | |
| const validationResponse = await fetch("/api/presets/validate", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify(presetData), | |
| }); | |
| if (!validationResponse.ok) { | |
| const error = await validationResponse.json(); | |
| throw new Error(error.error || "Validation failed"); | |
| } | |
| // Save to Firebase if user is authenticated | |
| if (app.user) { | |
| await database | |
| .ref(`users/${app.user.uid}/presets/${id}`) | |
| .set(presetData); | |
| console.log("Preset saved to Firebase"); | |
| } | |
| resetPresetForm(); | |
| toggleModal("presetsModal"); | |
| showToast("Preset saved successfully!", "success"); | |
| } catch (error) { | |
| console.error("Error saving preset:", error); | |
| showToast("Error saving preset: " + error.message, "error"); | |
| } | |
| } | |
| // Delete a preset | |
| async function deletePreset(presetId) { | |
| const preset = app.presets.find(p => p.id === presetId); | |
| if (preset && !preset.isCustom) { | |
| showToast("Cannot delete curated presets", "warning"); | |
| return; | |
| } | |
| if (!confirm("Are you sure you want to delete this preset?")) return; | |
| try { | |
| if (app.user) { | |
| await database | |
| .ref(`users/${app.user.uid}/presets/${presetId}`) | |
| .remove(); | |
| } | |
| if (app.activePresetId === presetId) { | |
| clearPreset(); | |
| } | |
| showToast("Preset deleted successfully", "success"); | |
| } catch (error) { | |
| console.error("Error deleting preset:", error); | |
| showToast("Error deleting preset", "error"); | |
| } | |
| } | |
| // Edit a preset | |
| function editPreset(presetId) { | |
| const preset = app.presets.find(p => p.id === presetId); | |
| if (!preset) return; | |
| app.editingPresetId = presetId; | |
| document.getElementById("presetName").value = preset.name; | |
| document.getElementById("presetDescription").value = preset.description || ""; | |
| document.getElementById("presetIcon").value = preset.icon || "🎯"; | |
| document.getElementById("presetModelId").value = preset.modelId; | |
| document.getElementById("presetSystemPrompt").value = preset.systemPrompt || ""; | |
| document.getElementById("presetTemperature").value = preset.temperature; | |
| document.getElementById("presetMaxTokens").value = preset.maxTokens; | |
| document.getElementById("presetTempValue").textContent = preset.temperature; | |
| app.presetKeywords = preset.keywords || []; | |
| renderPresetKeywords(); | |
| app.presetDatasets = preset.datasets || []; | |
| renderPresetDatasets(); | |
| document.getElementById("presetSaveBtn").textContent = "Update Preset"; | |
| toggleModal("presetsModal"); | |
| } | |
| // Duplicate a preset | |
| function duplicatePreset(sourceId) { | |
| const source = app.presets.find(p => p.id === sourceId); | |
| if (!source) return; | |
| const newId = `preset-${Date.now()}`; | |
| const duplicatePreset = { | |
| ...source, | |
| id: newId, | |
| name: `${source.name} (Copy)`, | |
| isCustom: true, | |
| createdAt: Date.now(), | |
| updatedAt: Date.now(), | |
| }; | |
| if (app.user) { | |
| database | |
| .ref(`users/${app.user.uid}/presets/${newId}`) | |
| .set(duplicatePreset); | |
| showToast("Preset duplicated successfully", "success"); | |
| } | |
| } | |
| // Reset preset form | |
| function resetPresetForm() { | |
| app.editingPresetId = null; | |
| app.presetKeywords = []; | |
| app.presetDatasets = []; | |
| document.getElementById("presetName").value = ""; | |
| document.getElementById("presetDescription").value = ""; | |
| document.getElementById("presetIcon").value = "🎯"; | |
| document.getElementById("presetModelId").value = "meta-llama/Llama-3.2-3B-Instruct"; | |
| document.getElementById("presetSystemPrompt").value = ""; | |
| document.getElementById("presetTemperature").value = 0.5; | |
| document.getElementById("presetTempValue").textContent = "0.5"; | |
| document.getElementById("presetMaxTokens").value = 500; | |
| document.getElementById("presetSaveBtn").textContent = "Save Preset"; | |
| renderPresetKeywords(); | |
| renderPresetDatasets(); | |
| } | |
| // Keyword management functions | |
| function renderPresetKeywords() { | |
| const container = document.getElementById("presetKeywordsContainer"); | |
| if (!container) return; | |
| if (app.presetKeywords.length === 0) { | |
| container.innerHTML = '<span class="text-gray-400 text-sm">No keywords. Add keywords above for auto-routing.</span>'; | |
| return; | |
| } | |
| container.innerHTML = app.presetKeywords | |
| .map((keyword, index) => ` | |
| <span class="keyword-tag"> | |
| ${keyword} | |
| <button onclick="removePresetKeyword(${index})">×</button> | |
| </span> | |
| `) | |
| .join(""); | |
| } | |
| function addPresetKeyword() { | |
| const input = document.getElementById("presetKeywordInput"); | |
| const keyword = input.value.trim().toLowerCase(); | |
| if (keyword && !app.presetKeywords.includes(keyword)) { | |
| app.presetKeywords.push(keyword); | |
| renderPresetKeywords(); | |
| input.value = ""; | |
| } | |
| } | |
| function removePresetKeyword(index) { | |
| app.presetKeywords.splice(index, 1); | |
| renderPresetKeywords(); | |
| } | |
| // Dataset management functions | |
| function renderPresetDatasets() { | |
| const container = document.getElementById("presetDatasetsContainer"); | |
| if (!container) return; | |
| if (app.presetDatasets.length === 0) { | |
| container.innerHTML = '<span class="text-gray-400 text-sm">No datasets added.</span>'; | |
| return; | |
| } | |
| container.innerHTML = app.presetDatasets | |
| .map((dataset, index) => ` | |
| <div class="dataset-item"> | |
| <div class="flex justify-between items-start"> | |
| <div> | |
| <p class="font-medium text-sm">${dataset.name || "Unnamed Dataset"}</p> | |
| <p class="text-xs text-gray-500">${dataset.url || "No URL"}</p> | |
| </div> | |
| <button class="btn btn-sm btn-ghost text-error" onclick="removePresetDataset(${index})"> | |
| <span class="material-icons text-sm">delete</span> | |
| </button> | |
| </div> | |
| </div> | |
| `) | |
| .join(""); | |
| } | |
| function addPresetDataset() { | |
| const name = document.getElementById("presetDatasetName").value.trim(); | |
| const url = document.getElementById("presetDatasetUrl").value.trim(); | |
| if (name && url) { | |
| app.presetDatasets.push({ name, url }); | |
| renderPresetDatasets(); | |
| document.getElementById("presetDatasetName").value = ""; | |
| document.getElementById("presetDatasetUrl").value = ""; | |
| } | |
| } | |
| function removePresetDataset(index) { | |
| app.presetDatasets.splice(index, 1); | |
| renderPresetDatasets(); | |
| } | |
| function toggleDatasetAccordion() { | |
| const content = document.getElementById("presetDatasetAccordion"); | |
| content.classList.toggle("hidden"); | |
| } | |
| // Preset rendering functions | |
| function renderPresetSelector() { | |
| const select = document.getElementById("presetSelect"); | |
| if (!select) return; | |
| // Group presets | |
| const curated = app.curatedPresets; | |
| const custom = app.userPresets; | |
| let html = `<option value="">No Preset</option>`; | |
| if (curated.length > 0) { | |
| html += `<optgroup label="🌟 Curated Presets">`; | |
| curated.forEach(preset => { | |
| const selected = app.activePresetId === preset.id ? "selected" : ""; | |
| html += `<option value="${preset.id}" ${selected}>${preset.icon} ${preset.name}</option>`; | |
| }); | |
| html += `</optgroup>`; | |
| } | |
| if (custom.length > 0) { | |
| html += `<optgroup label="👤 My Presets">`; | |
| custom.forEach(preset => { | |
| const selected = app.activePresetId === preset.id ? "selected" : ""; | |
| html += `<option value="${preset.id}" ${selected}>${preset.icon || "🎯"} ${preset.name}</option>`; | |
| }); | |
| html += `</optgroup>`; | |
| } | |
| select.innerHTML = html; | |
| } | |
| function renderPresetManager() { | |
| const container = document.getElementById("presetManagerContent"); | |
| if (!container) return; | |
| const curated = app.curatedPresets; | |
| const custom = app.userPresets; | |
| let html = ""; | |
| if (curated.length > 0) { | |
| html += `<div class="mb-6"> | |
| <h4 class="font-semibold text-lg mb-3 flex items-center"> | |
| <span class="material-icons mr-2">auto_awesome</span>Curated Presets | |
| </h4> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| ${curated.map(p => renderPresetCard(p)).join("")} | |
| </div> | |
| </div>`; | |
| } | |
| if (custom.length > 0) { | |
| html += `<div> | |
| <h4 class="font-semibold text-lg mb-3 flex items-center"> | |
| <span class="material-icons mr-2">person</span>My Presets (${custom.length}) | |
| </h4> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| ${custom.map(p => renderPresetCard(p)).join("")} | |
| </div> | |
| </div>`; | |
| } | |
| if (!html) { | |
| html = '<div class="text-center py-8 text-gray-500">No presets available</div>'; | |
| } | |
| container.innerHTML = html; | |
| } | |
| function renderPresetCard(preset) { | |
| const isActive = app.activePresetId === preset.id; | |
| const isCustom = preset.isCustom; | |
| return ` | |
| <div class="model-card bg-white border rounded-lg p-4 ${isActive ? "border-primary bg-primary/5" : "border-gray-200"}"> | |
| <div class="flex justify-between items-start mb-2"> | |
| <div class="flex items-center gap-2"> | |
| <span class="text-2xl">${preset.icon || "🎯"}</span> | |
| <div> | |
| <h4 class="font-semibold">${preset.name}</h4> | |
| <p class="text-xs text-gray-500">${preset.description || preset.modelId.split("/").pop()}</p> | |
| </div> | |
| </div> | |
| <div class="flex gap-1"> | |
| ${isActive ? '<span class="badge badge-sm badge-primary">Active</span>' : ''} | |
| <button class="btn btn-sm btn-ghost btn-circle" onclick="selectPreset('${preset.id}')"> | |
| <span class="material-icons text-sm">play_arrow</span> | |
| </button> | |
| <button class="btn btn-sm btn-ghost btn-circle" onclick="duplicatePreset('${preset.id}')"> | |
| <span class="material-icons text-sm">content_copy</span> | |
| </button> | |
| ${isCustom ? ` | |
| <button class="btn btn-sm btn-ghost btn-circle" onclick="editPreset('${preset.id}')"> | |
| <span class="material-icons text-sm">edit</span> | |
| </button> | |
| <button class="btn btn-sm btn-ghost btn-circle text-error" onclick="deletePreset('${preset.id}')"> | |
| <span class="material-icons text-sm">delete</span> | |
| </button> | |
| ` : ''} | |
| </div> | |
| </div> | |
| <p class="text-xs text-gray-400 truncate">${preset.systemPrompt?.substring(0, 60) || "No system prompt"}...</p> | |
| ${preset.keywords?.length > 0 ? ` | |
| <div class="flex flex-wrap gap-1 mt-2"> | |
| ${preset.keywords.slice(0, 3).map(kw => `<span class="badge badge-xs badge-ghost">${kw}</span>`).join("")} | |
| ${preset.keywords.length > 3 ? `<span class="badge badge-xs badge-ghost">+${preset.keywords.length - 3}</span>` : ''} | |
| </div> | |
| ` : ''} | |
| </div> | |
| `; | |
| } | |
| function selectModel(modelId) { | |
| app.selectedModelId = modelId; | |
| // Update dropdown | |
| const select = document.getElementById("modelSelect"); | |
| select.value = modelId; | |
| // Re-render active models to show selection | |
| renderActiveModels(); | |
| // Re-render quick switcher | |
| const enabledModels = app.models.filter((m) => m.enabled); | |
| renderQuickSwitcher(enabledModels); | |
| // Update header display | |
| updateSelectedModelDisplay(); | |
| // Save selection to localStorage | |
| localStorage.setItem("selectedModelId", modelId); | |
| console.log("Selected model:", modelId); | |
| } | |
| function updateSelectedModelDisplay() { | |
| const nameEl = document.getElementById("selectedModelName"); | |
| const roleEl = document.getElementById("selectedModelRole"); | |
| if (app.selectedModelId) { | |
| const model = app.models.find((m) => m.id === app.selectedModelId); | |
| if (model) { | |
| nameEl.textContent = model.name; | |
| roleEl.textContent = `${model.role} • ${model.modelId.split("/").pop()}`; | |
| return; | |
| } | |
| } | |
| // Default display | |
| const defaultModel = | |
| modelSettings.defaultModel || "meta-llama/Llama-3.2-3B-Instruct"; | |
| nameEl.textContent = "AI Assistant"; | |
| roleEl.textContent = `Powered by ${defaultModel.split("/").pop()}`; | |
| } | |
| function getSelectedModel() { | |
| // Check if a preset is active and merge its settings | |
| if (app.activePresetId) { | |
| const preset = app.presets.find((p) => p.id === app.activePresetId); | |
| if (preset) { | |
| return { | |
| modelId: preset.modelId, | |
| temperature: preset.temperature, | |
| maxTokens: preset.maxTokens, | |
| systemPrompt: preset.systemPrompt, | |
| name: preset.name, | |
| keywords: preset.keywords, | |
| }; | |
| } | |
| } | |
| if (app.selectedModelId) { | |
| const model = app.models.find((m) => m.id === app.selectedModelId); | |
| if (model) return model; | |
| } | |
| // Return default model config | |
| return { | |
| modelId: | |
| modelSettings.defaultModel || "meta-llama/Llama-3.2-3B-Instruct", | |
| temperature: 0.3, | |
| maxTokens: 500, | |
| systemPrompt: "", | |
| name: "AI Assistant", | |
| }; | |
| } | |
| async function saveModel() { | |
| const name = document.getElementById("modelName").value.trim(); | |
| const modelId = document.getElementById("modelId").value.trim(); | |
| if (!name || !modelId) { | |
| alert("Please fill in required fields"); | |
| return; | |
| } | |
| const id = app.editingModelId || `model-${Date.now()}`; | |
| const modelData = { | |
| id, | |
| name, | |
| modelId, | |
| role: document.getElementById("modelRole").value, | |
| temperature: parseFloat(document.getElementById("modelTemp").value), | |
| maxTokens: parseInt(document.getElementById("modelMaxTokens").value), | |
| systemPrompt: document.getElementById("modelSystemPrompt").value, | |
| keywords: modelKeywords, | |
| datasets: [], | |
| enabled: document.getElementById("modelEnabled").checked, | |
| createdAt: app.editingModelId ? undefined : Date.now(), | |
| updatedAt: Date.now(), | |
| }; | |
| try { | |
| // Save to settings file via API | |
| const settingsResponse = await fetch("/api/settings/models/add", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify(modelData), | |
| }); | |
| if (!settingsResponse.ok) { | |
| const error = await settingsResponse.json(); | |
| throw new Error(error.error || "Failed to save to settings"); | |
| } | |
| console.log("Model saved to settings file"); | |
| // Also save to Firebase if user is authenticated | |
| if (app.user) { | |
| await database | |
| .ref(`users/${app.user.uid}/models/${id}`) | |
| .set(modelData); | |
| console.log("Model saved to Firebase"); | |
| } | |
| // Reload settings | |
| await loadAllSettings(); | |
| resetModelForm(); | |
| loadModels(); | |
| // Show success message | |
| alert("Model saved successfully!"); | |
| } catch (error) { | |
| console.error("Error saving model:", error); | |
| alert("Error saving model: " + error.message); | |
| } | |
| } | |
| async function refreshModelsRealtime() { | |
| const btn = document.getElementById("refreshModelsBtn"); | |
| if (!btn) return; | |
| btn.disabled = true; | |
| btn.innerHTML = | |
| '<span class="material-icons animate-spin">refresh</span>Refreshing...'; | |
| try { | |
| // Fetch latest model info from server | |
| const response = await fetch("/api/search-models?limit=20"); | |
| const data = await response.json(); | |
| if (data.models && data.models.length > 0) { | |
| // Update model info with latest data from HuggingFace | |
| for (const model of app.models) { | |
| const hfModel = data.models.find((m) => m.id === model.modelId); | |
| if (hfModel) { | |
| model.downloads = hfModel.downloads; | |
| model.likes = hfModel.likes; | |
| model.lastUpdated = Date.now(); | |
| } | |
| } | |
| renderModels(); | |
| alert("✓ Models updated with latest info from HuggingFace!"); | |
| } | |
| } catch (error) { | |
| console.error("Error refreshing models:", error); | |
| alert("Failed to refresh models: " + error.message); | |
| } finally { | |
| btn.disabled = false; | |
| btn.innerHTML = | |
| '<span class="material-icons">refresh</span>Refresh Models'; | |
| } | |
| } | |
| async function searchHFModels() { | |
| const searchInput = document.getElementById("modelSearchInput"); | |
| const query = searchInput.value.trim(); | |
| if (!query) { | |
| alert("Please enter a search query"); | |
| return; | |
| } | |
| const searchBtn = document.getElementById("searchModelsBtn"); | |
| searchBtn.disabled = true; | |
| searchBtn.innerHTML = | |
| '<span class="material-icons animate-spin">search</span>Searching...'; | |
| try { | |
| const response = await fetch( | |
| `/api/search-models?search=${encodeURIComponent(query)}&limit=10`, | |
| ); | |
| const data = await response.json(); | |
| if (data.models && data.models.length > 0) { | |
| displaySearchResults(data.models); | |
| } else { | |
| alert("No models found matching your search"); | |
| } | |
| } catch (error) { | |
| console.error("Error searching models:", error); | |
| alert("Search failed: " + error.message); | |
| } finally { | |
| searchBtn.disabled = false; | |
| searchBtn.innerHTML = | |
| '<span class="material-icons">search</span>Search'; | |
| } | |
| } | |
| function displaySearchResults(models) { | |
| const resultsContainer = document.getElementById( | |
| "searchResultsContainer", | |
| ); | |
| if (!resultsContainer) { | |
| alert("Search results container not found"); | |
| return; | |
| } | |
| resultsContainer.classList.remove("hidden"); | |
| resultsContainer.innerHTML = ` | |
| <div class="bg-white rounded-lg border border-primary/20 p-4 mb-4"> | |
| <h4 class="font-semibold mb-3">Found ${models.length} Models</h4> | |
| <div class="space-y-2 max-h-60 overflow-y-auto"> | |
| ${models | |
| .map( | |
| (model) => ` | |
| <div class="flex items-center justify-between p-2 hover:bg-gray-50 rounded-lg transition-colors border border-gray-200"> | |
| <div class="flex-1 min-w-0"> | |
| <p class="font-medium truncate">${model.name || model.id}</p> | |
| <p class="text-xs text-gray-500 truncate">${model.id}</p> | |
| <div class="flex gap-2 mt-1 text-xs text-gray-400"> | |
| <span>📥 ${(model.downloads || 0).toLocaleString()}</span> | |
| <span>❤️ ${model.likes || 0}</span> | |
| </div> | |
| </div> | |
| <button class="btn btn-sm btn-primary" onclick="quickAddModel('${model.id}', '${(model.name || model.id).replace(/'/g, "\\'")}')"> | |
| Add | |
| </button> | |
| </div> | |
| `, | |
| ) | |
| .join("")} | |
| </div> | |
| </div> | |
| `; | |
| } | |
| function quickAddModel(modelId, modelName) { | |
| document.getElementById("modelId").value = modelId; | |
| document.getElementById("modelName").value = modelName; | |
| // Fetch model info if available | |
| fetch(`/api/model-info?model_id=${encodeURIComponent(modelId)}`) | |
| .then((r) => r.json()) | |
| .then((data) => { | |
| if (data.summary) { | |
| document.getElementById("modelSystemPrompt").value = | |
| data.summary.substring(0, 200); | |
| } | |
| }) | |
| .catch((e) => console.log("Could not fetch model info:", e)); | |
| // Hide search results | |
| const resultsContainer = document.getElementById( | |
| "searchResultsContainer", | |
| ); | |
| if (resultsContainer) resultsContainer.classList.add("hidden"); | |
| alert( | |
| `Model "${modelName}" added to form. Customize settings and click Add Model.`, | |
| ); | |
| } | |
| function editModel(modelId) { | |
| const model = app.models.find((m) => m.id === modelId); | |
| if (!model) return; | |
| app.editingModelId = modelId; | |
| document.getElementById("modelFormTitle").textContent = "Edit Model"; | |
| document.getElementById("modelName").value = model.name; | |
| document.getElementById("modelId").value = model.modelId; | |
| document.getElementById("modelRole").value = model.role; | |
| document.getElementById("modelTemp").value = model.temperature; | |
| document.getElementById("tempValue").textContent = model.temperature; | |
| document.getElementById("modelMaxTokens").value = model.maxTokens; | |
| document.getElementById("modelSystemPrompt").value = | |
| model.systemPrompt || ""; | |
| document.getElementById("modelEnabled").checked = model.enabled; | |
| document.getElementById("saveModelBtn").innerHTML = | |
| '<span class="material-icons text-sm mr-1">save</span>Update Model'; | |
| document.getElementById("cancelEditBtn").style.display = "block"; | |
| // Load keywords for editing | |
| modelKeywords = model.keywords || []; | |
| renderKeywords(); | |
| // Hide model info panel when editing | |
| document.getElementById("modelInfoPanel").classList.add("hidden"); | |
| document.getElementById("modelInfoStatus").innerHTML = ""; | |
| } | |
| async function deleteModel(modelId) { | |
| if (!confirm("Are you sure you want to delete this model?")) return; | |
| try { | |
| // Delete from settings file | |
| const settingsResponse = await fetch( | |
| `/api/settings/models/${modelId}`, | |
| { | |
| method: "DELETE", | |
| }, | |
| ); | |
| if (!settingsResponse.ok) { | |
| console.log("Could not delete from settings file"); | |
| } | |
| // Delete from Firebase if user is authenticated | |
| if (app.user) { | |
| try { | |
| await database | |
| .ref(`users/${app.user.uid}/models/${modelId}`) | |
| .remove(); | |
| } catch (fbError) { | |
| console.log("Could not delete from Firebase:", fbError.message); | |
| } | |
| } | |
| // Reload settings | |
| await loadAllSettings(); | |
| app.models = app.models.filter((m) => m.id !== modelId); | |
| renderModels(); | |
| renderModelSelect(); | |
| } catch (error) { | |
| console.error("Error deleting model:", error); | |
| } | |
| } | |
| function cancelEditModel() { | |
| resetModelForm(); | |
| } | |
| function resetModelForm() { | |
| app.editingModelId = null; | |
| document.getElementById("modelFormTitle").textContent = "Add New Model"; | |
| document.getElementById("modelName").value = "Llama 3.2"; | |
| document.getElementById("modelId").value = | |
| "meta-llama/Llama-3.2-3B-Instruct"; | |
| document.getElementById("modelRole").value = "assistant"; | |
| document.getElementById("modelTemp").value = 0.3; | |
| document.getElementById("tempValue").textContent = "0.3"; | |
| document.getElementById("modelMaxTokens").value = 500; | |
| document.getElementById("modelSystemPrompt").value = ""; | |
| document.getElementById("modelEnabled").checked = true; | |
| document.getElementById("saveModelBtn").innerHTML = | |
| '<span class="material-icons text-sm mr-1">add</span>Add Model'; | |
| document.getElementById("cancelEditBtn").style.display = "none"; | |
| // Reset keywords | |
| modelKeywords = []; | |
| renderKeywords(); | |
| // Reset search | |
| const searchInput = document.getElementById("modelSearchInput"); | |
| if (searchInput) searchInput.value = ""; | |
| const resultsContainer = document.getElementById( | |
| "searchResultsContainer", | |
| ); | |
| if (resultsContainer) resultsContainer.classList.add("hidden"); | |
| // Hide model info panel | |
| document.getElementById("modelInfoPanel").classList.add("hidden"); | |
| document.getElementById("modelInfoStatus").innerHTML = ""; | |
| } | |
| async function sendMessage() { | |
| const input = document.getElementById("messageInput"); | |
| const message = input.value.trim(); | |
| if (!message) return; | |
| app.isTyping = true; | |
| input.disabled = true; | |
| const userMessage = { | |
| text: message, | |
| sender: "user", | |
| timestamp: Date.now(), | |
| mode: app.chatMode, | |
| }; | |
| app.messages.push(userMessage); | |
| renderMessages(); | |
| saveMessage(userMessage); | |
| input.value = ""; | |
| // Get selected model with its settings | |
| const selectedModel = getSelectedModel(); | |
| const modelId = selectedModel.modelId; | |
| const apiKey = document.getElementById("apiKey").value; | |
| // Use model-specific settings or fallback to global | |
| const temperature = | |
| selectedModel.temperature || | |
| parseFloat(document.getElementById("globalTemp").value); | |
| const maxTokens = selectedModel.maxTokens || 500; | |
| const systemPrompt = selectedModel.systemPrompt || ""; | |
| try { | |
| let botMessage; | |
| if (app.chatMode === "chat") { | |
| // Standard chat mode | |
| let finalInput = message; | |
| if (systemPrompt) { | |
| finalInput = `System: ${systemPrompt}\n\nUser: ${message}`; | |
| } | |
| const response = await fetch("/api/chat", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ | |
| model_id: modelId, | |
| api_key: apiKey, | |
| inputs: finalInput, | |
| parameters: { | |
| temperature: temperature, | |
| max_new_tokens: maxTokens, | |
| return_full_text: false, | |
| }, | |
| }), | |
| }); | |
| const data = await response.json(); | |
| if (data.error) throw new Error(data.error); | |
| const aiResponse = | |
| data[0]?.generated_text || | |
| "Sorry, I could not generate a response."; | |
| botMessage = { | |
| text: aiResponse, | |
| sender: "bot", | |
| timestamp: Date.now(), | |
| modelName: selectedModel.name || "AI Assistant", | |
| mode: "chat", | |
| }; | |
| } else if (app.chatMode === "text-generation") { | |
| const response = await fetch("/api/text-generation", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ | |
| model_id: modelId, | |
| prompt: message, | |
| parameters: { | |
| max_new_tokens: maxTokens, | |
| temperature: temperature, | |
| }, | |
| }), | |
| }); | |
| const data = await response.json(); | |
| if (data.error) throw new Error(data.error); | |
| botMessage = { | |
| text: data.generated_text || "No text generated.", | |
| sender: "bot", | |
| timestamp: Date.now(), | |
| modelName: selectedModel.name || "Text Generator", | |
| mode: "text-generation", | |
| }; | |
| } else if (app.chatMode === "image-generation") { | |
| const imageModelId = document.getElementById("imageModel").value; | |
| const negativePrompt = | |
| document.getElementById("negativePrompt").value; | |
| // Show loading message | |
| const loadingMsg = { | |
| text: "🎨 Generating image...", | |
| sender: "bot", | |
| timestamp: Date.now(), | |
| mode: "image-generation", | |
| }; | |
| app.messages.push(loadingMsg); | |
| renderMessages(); | |
| const response = await fetch("/api/image-generation", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ | |
| model_id: imageModelId, | |
| prompt: message, | |
| negative_prompt: negativePrompt || null, | |
| parameters: { num_inference_steps: 30 }, | |
| }), | |
| }); | |
| const data = await response.json(); | |
| // Remove loading message | |
| app.messages.pop(); | |
| if (data.error) throw new Error(data.error); | |
| botMessage = { | |
| text: `<img src="data:image/png;base64,${data.image}" class="max-w-full rounded-lg" alt="${message}" /><p class="text-xs mt-2 opacity-70">Prompt: ${message}</p>`, | |
| sender: "bot", | |
| timestamp: Date.now(), | |
| modelName: imageModelId.split("/").pop(), | |
| mode: "image-generation", | |
| isHtml: true, | |
| }; | |
| } else if (app.chatMode === "translation") { | |
| const translateModelId = | |
| document.getElementById("translateModel").value; | |
| const response = await fetch("/api/translation", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ | |
| model_id: translateModelId, | |
| text: message, | |
| }), | |
| }); | |
| const data = await response.json(); | |
| if (data.error) throw new Error(data.error); | |
| botMessage = { | |
| text: `📝 **Translation:**\n${data.translation}\n\n📄 **Original:**\n${data.original}`, | |
| sender: "bot", | |
| timestamp: Date.now(), | |
| modelName: translateModelId.split("/").pop(), | |
| mode: "translation", | |
| }; | |
| } else if (app.chatMode === "question-answering") { | |
| const context = document | |
| .getElementById("contextInput") | |
| .value.trim(); | |
| if (!context) { | |
| throw new Error("Please provide context for question answering."); | |
| } | |
| const response = await fetch("/api/question-answering", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ | |
| model_id: "deepset/roberta-base-squad2", | |
| question: message, | |
| context: context, | |
| }), | |
| }); | |
| const data = await response.json(); | |
| if (data.error) throw new Error(data.error); | |
| const confidence = (data.score * 100).toFixed(1); | |
| botMessage = { | |
| text: `✅ **Answer:** ${data.answer}\n\n📊 **Confidence:** ${confidence}%`, | |
| sender: "bot", | |
| timestamp: Date.now(), | |
| modelName: "Q&A Model", | |
| mode: "question-answering", | |
| }; | |
| } else if (app.chatMode === "summarization") { | |
| const response = await fetch("/api/summarization", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ | |
| model_id: "facebook/bart-large-cnn", | |
| text: message, | |
| parameters: { max_length: 150, min_length: 30 }, | |
| }), | |
| }); | |
| const data = await response.json(); | |
| if (data.error) throw new Error(data.error); | |
| botMessage = { | |
| text: `📋 **Summary:**\n${data.summary}`, | |
| sender: "bot", | |
| timestamp: Date.now(), | |
| modelName: "Summarizer", | |
| mode: "summarization", | |
| }; | |
| } | |
| app.messages.push(botMessage); | |
| renderMessages(); | |
| saveMessage(botMessage); | |
| } catch (error) { | |
| console.error("Error getting AI response:", error); | |
| const errorMessage = { | |
| text: `Sorry, I encountered an error: ${error.message}. Please check your API key and try again.`, | |
| sender: "bot", | |
| timestamp: Date.now(), | |
| mode: app.chatMode, | |
| }; | |
| app.messages.push(errorMessage); | |
| renderMessages(); | |
| } finally { | |
| app.isTyping = false; | |
| input.disabled = false; | |
| input.focus(); | |
| scrollToBottom(); | |
| } | |
| } | |
| function setChatMode(mode) { | |
| app.chatMode = mode; | |
| // Update tab styles | |
| document | |
| .querySelectorAll(".tabs .tab") | |
| .forEach((tab) => tab.classList.remove("tab-active")); | |
| document | |
| .getElementById("mode" + getModeTabId(mode)) | |
| .classList.add("tab-active"); | |
| // Show/hide mode-specific inputs | |
| document | |
| .getElementById("contextInputArea") | |
| .classList.toggle("hidden", mode !== "question-answering"); | |
| document | |
| .getElementById("translationOptions") | |
| .classList.toggle("hidden", mode !== "translation"); | |
| document | |
| .getElementById("imageOptions") | |
| .classList.toggle("hidden", mode !== "image-generation"); | |
| // Update placeholder | |
| const placeholders = { | |
| chat: "Type your message...", | |
| "text-generation": "Enter a prompt to continue...", | |
| "image-generation": "Describe the image you want to generate...", | |
| translation: "Enter text to translate...", | |
| "question-answering": "Ask a question about the context...", | |
| summarization: "Paste text to summarize...", | |
| }; | |
| document.getElementById("messageInput").placeholder = | |
| placeholders[mode] || "Type your message..."; | |
| console.log("Chat mode set to:", mode); | |
| } | |
| function getModeTabId(mode) { | |
| const ids = { | |
| chat: "Chat", | |
| "text-generation": "TextGen", | |
| "image-generation": "Image", | |
| translation: "Translate", | |
| "question-answering": "QA", | |
| summarization: "Summary", | |
| }; | |
| return ids[mode] || "Chat"; | |
| } | |
| function quickReply(text) { | |
| document.getElementById("messageInput").value = text; | |
| sendMessage(); | |
| } | |
| function renderMessages() { | |
| const messageArea = document.getElementById("messageArea"); | |
| messageArea.innerHTML = app.messages | |
| .map((msg) => { | |
| const modeBadge = | |
| msg.mode && msg.mode !== "chat" | |
| ? `<span class="badge badge-xs badge-primary ml-2">${getModeBadgeLabel(msg.mode)}</span>` | |
| : ""; | |
| const content = msg.isHtml | |
| ? msg.text | |
| : `<p class="text-sm whitespace-pre-wrap">${formatMessageText(msg.text)}</p>`; | |
| return `<div class="flex ${msg.sender === "user" ? "justify-end" : "justify-start"}"><div class="max-w-xs lg:max-w-md xl:max-w-lg"><div class="rounded-lg px-4 py-3 shadow-md ${msg.sender === "user" ? "gradient-bg text-white" : "bg-gray-100"}"><div class="message-content">${content}</div><div class="flex items-center justify-between mt-1 flex-wrap gap-1"><p class="text-xs opacity-70">${formatTime(msg.timestamp)}</p>${msg.modelName ? `<span class="text-xs opacity-70 ml-2">• ${msg.modelName}</span>` : ""}${modeBadge}</div></div></div></div>`; | |
| }) | |
| .join(""); | |
| scrollToBottom(); | |
| } | |
| function getModeBadgeLabel(mode) { | |
| const labels = { | |
| "text-generation": "📝 Text", | |
| "image-generation": "🎨 Image", | |
| translation: "🌐 Translate", | |
| "question-answering": "❓ Q&A", | |
| summarization: "📋 Summary", | |
| }; | |
| return labels[mode] || mode; | |
| } | |
| function formatMessageText(text) { | |
| // Simple markdown-like formatting | |
| return text | |
| .replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>") | |
| .replace(/\n/g, "<br>"); | |
| } | |
| function scrollToBottom() { | |
| const messageArea = document.getElementById("messageArea"); | |
| messageArea.scrollTop = messageArea.scrollHeight; | |
| } | |
| async function loadChatSessions() { | |
| try { | |
| const snapshot = await database | |
| .ref(`chats/${app.user.uid}`) | |
| .once("value"); | |
| const sessions = snapshot.val() || {}; | |
| app.chatSessions = Object.values(sessions) | |
| .sort((a, b) => b.timestamp - a.timestamp) | |
| .slice(0, 10); | |
| renderChatHistory(); | |
| } catch (error) { | |
| console.error("Error loading chat sessions:", error); | |
| } | |
| } | |
| function renderChatHistory() { | |
| const chatHistory = document.getElementById("chatHistory"); | |
| chatHistory.innerHTML = app.chatSessions | |
| .map((session) => { | |
| const messageCount = session.messages | |
| ? Object.keys(session.messages).length | |
| : 0; | |
| const preview = session.lastMessage | |
| ? session.lastMessage.substring(0, 40) + | |
| (session.lastMessage.length > 40 ? "..." : "") | |
| : "No messages"; | |
| return `<div class="p-3 rounded-lg hover:bg-gray-100 cursor-pointer transition-colors border-l-4 border-transparent hover:border-primary ${app.currentSessionId === session.id ? "bg-primary/10 border-primary" : ""}" onclick="loadSession('${session.id}')"> | |
| <div class="flex items-center justify-between"> | |
| <p class="text-sm font-medium truncate flex-1">${session.title}</p> | |
| <button onclick="event.stopPropagation(); deleteSession('${session.id}')" class="btn btn-ghost btn-xs text-error opacity-0 group-hover:opacity-100 hover:opacity-100">×</button> | |
| </div> | |
| <p class="text-xs text-gray-600 truncate">${preview}</p> | |
| <div class="flex items-center justify-between mt-1"> | |
| <p class="text-xs text-gray-400">${formatTime(session.timestamp)}</p> | |
| <span class="badge badge-xs badge-ghost">${messageCount} msgs</span> | |
| </div> | |
| </div>`; | |
| }) | |
| .join(""); | |
| } | |
| async function deleteSession(sessionId) { | |
| if (!confirm("Delete this chat session?")) return; | |
| try { | |
| await database.ref(`chats/${app.user.uid}/${sessionId}`).remove(); | |
| if (app.currentSessionId === sessionId) { | |
| startNewSession(); | |
| } | |
| loadChatSessions(); | |
| } catch (error) { | |
| console.error("Error deleting session:", error); | |
| } | |
| } | |
| async function loadSession(sessionId) { | |
| try { | |
| const snapshot = await database | |
| .ref(`chats/${app.user.uid}/${sessionId}`) | |
| .once("value"); | |
| const session = snapshot.val(); | |
| if (session) { | |
| app.currentSessionId = sessionId; | |
| app.messages = Object.values(session.messages || {}); | |
| renderMessages(); | |
| } | |
| } catch (error) { | |
| console.error("Error loading session:", error); | |
| } | |
| } | |
| function startNewSession() { | |
| app.currentSessionId = Date.now().toString(); | |
| app.messages = []; | |
| const session = { | |
| id: app.currentSessionId, | |
| title: "New Chat", | |
| timestamp: Date.now(), | |
| messages: [], | |
| }; | |
| database | |
| .ref(`chats/${app.user.uid}/${app.currentSessionId}`) | |
| .set(session); | |
| loadChatSessions(); | |
| renderMessages(); | |
| } | |
| function saveMessage(message) { | |
| if (!app.currentSessionId) return; | |
| database | |
| .ref(`chats/${app.user.uid}/${app.currentSessionId}/messages`) | |
| .push(message); | |
| database | |
| .ref(`chats/${app.user.uid}/${app.currentSessionId}`) | |
| .update({ lastMessage: message.text, timestamp: message.timestamp }); | |
| } | |
| function shareChat() { | |
| if (navigator.share) { | |
| navigator | |
| .share({ | |
| title: "AI Chat Conversation", | |
| text: "Check out my conversation with AI Assistant", | |
| url: window.location.href, | |
| }) | |
| .catch((error) => console.log("Share cancelled or failed:", error)); | |
| } else { | |
| const chatText = app.messages | |
| .map((m) => `${m.sender}: ${m.text}`) | |
| .join("\n"); | |
| navigator.clipboard.writeText(chatText).then(() => { | |
| alert("Chat copied to clipboard!"); | |
| }); | |
| } | |
| } | |
| function formatTime(timestamp) { | |
| const date = new Date(timestamp); | |
| return date.toLocaleTimeString("en-US", { | |
| hour: "2-digit", | |
| minute: "2-digit", | |
| }); | |
| } | |
| function loadApiKey() { | |
| const savedApiKey = localStorage.getItem("huggingface_api_key"); | |
| if (savedApiKey) { | |
| document.getElementById("apiKey").value = savedApiKey; | |
| } | |
| // Load saved model selection | |
| const savedModelId = localStorage.getItem("selectedModelId"); | |
| if (savedModelId) { | |
| app.selectedModelId = savedModelId; | |
| } | |
| } | |
| // Handle model select dropdown change | |
| document.addEventListener("DOMContentLoaded", () => { | |
| const modelSelect = document.getElementById("modelSelect"); | |
| if (modelSelect) { | |
| modelSelect.addEventListener("change", (e) => { | |
| selectModel(e.target.value); | |
| }); | |
| } | |
| }); | |
| </script> | |
| </body> | |
| </html> | |