New_folder_2 / index.html
zxc4wewewe's picture
Upload 12 files
621ec47 verified
<!doctype html>
<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">&times;</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})">&times;</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>