mirror of
https://github.com/ParisNeo/lollms_hub.git
synced 2026-05-04 03:01:01 -04:00
This commit introduces several related updates across the application, focusing on establishing core functionality for bot management, admin routing, and metadata handling. Key changes include: - Updating API routes for admin and proxy functionality. - Refactoring the `BotManager` to handle the starting of active bots. - Implementing necessary CRUD operations for model metadata and server information. - Updating database migration and session management files. - Adjusting relevant template files for the admin dashboard display.
626 lines
38 KiB
HTML
626 lines
38 KiB
HTML
{% extends "admin/base.html" %}
|
|
|
|
{% block title %}Models Manager{% endblock %}
|
|
|
|
{% block header_title %}Models Manager & Auto-Routing{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="space-y-8">
|
|
<!-- AI Auto-Config Section -->
|
|
<div class="card-style border-l-4 border-purple-600 bg-purple-600/5 mb-8">
|
|
<div class="flex flex-col md:flex-row justify-between items-center gap-6">
|
|
<div class="flex items-center gap-4">
|
|
<div class="p-3 bg-purple-600 rounded-2xl shadow-xl shadow-purple-500/20">
|
|
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path></svg>
|
|
</div>
|
|
<div>
|
|
<h2 class="text-xl font-black text-white uppercase tracking-tighter">AI Auto-Config</h2>
|
|
<p class="text-xs text-purple-300 font-bold uppercase tracking-widest">Ground model metadata in industry standard scorecards</p>
|
|
</div>
|
|
</div>
|
|
<div class="flex flex-grow max-w-xl gap-2">
|
|
<input type="text" id="ai-config-url" value="https://grumpified-oggvct.github.io/model-trust-scorecard/" class="flex-grow bg-black/40 border border-purple-500/30 rounded-xl px-4 py-2 text-sm text-white outline-none focus:border-purple-500">
|
|
<button onclick="runAIConfig()" id="ai-config-btn" class="bg-purple-600 hover:bg-purple-500 text-white px-6 py-2 rounded-xl font-black text-xs uppercase tracking-widest transition-all transform active:scale-95 shadow-lg shadow-purple-900/20">Sync Metadata</button>
|
|
<button onclick="stopAIConfig()" id="ai-stop-btn" class="hidden bg-red-600 hover:bg-red-500 text-white px-4 py-2 rounded-xl font-black text-xs uppercase transition-all shadow-lg">STOP</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="ai-config-progress-container" class="hidden mt-6">
|
|
<div class="flex justify-between text-[10px] font-black text-purple-400 uppercase tracking-widest mb-1">
|
|
<span id="ai-progress-label">Syncing Cluster Intelligence...</span>
|
|
<span id="ai-progress-percent">0%</span>
|
|
</div>
|
|
<div class="h-1.5 w-full bg-black/40 rounded-full border border-white/5 overflow-hidden">
|
|
<div id="ai-progress-bar" class="h-full bg-purple-600 transition-all duration-500 shadow-[0_0_10px_rgba(168,85,247,0.5)]" style="width: 0%"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="ai-config-console" class="hidden mt-4 bg-black/60 border border-white/5 rounded-xl p-4 font-mono text-[10px] text-purple-300 space-y-1 h-32 overflow-y-auto custom-scrollbar">
|
|
<div class="text-gray-600 italic">Waiting for architect...</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Server Filter Bar -->
|
|
<div class="card-style mb-6">
|
|
<div class="flex flex-col gap-4">
|
|
<div class="flex items-center justify-between">
|
|
<h3 class="text-xs font-black text-gray-500 uppercase tracking-widest">Filter by Infrastructure</h3>
|
|
<button onclick="clearServerFilters()" class="text-[10px] font-bold text-indigo-400 hover:text-white uppercase">Reset Filters</button>
|
|
</div>
|
|
<div class="flex flex-wrap gap-2" id="server-filter-container">
|
|
<button onclick="toggleServerFilter('all', this)" class="server-filter-btn px-4 py-2 rounded-xl text-[10px] font-black uppercase tracking-widest border border-indigo-500/30 bg-indigo-600 text-white shadow-lg shadow-indigo-900/20 transition-all" data-server-id="all">
|
|
All Servers
|
|
</button>
|
|
{% for server in servers %}
|
|
<button onclick="toggleServerFilter({{ server.id }}, this)" class="server-filter-btn px-4 py-2 rounded-xl text-[10px] font-black uppercase tracking-widest border border-white/5 bg-white/5 text-gray-500 hover:text-indigo-300 transition-all" data-server-id="{{ server.id }}">
|
|
{{ server.name }}
|
|
</button>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card-style">
|
|
<div class="flex justify-between items-center">
|
|
<div class="flex items-center gap-4 cursor-pointer" onclick="document.getElementById('doc-body').classList.toggle('hidden'); this.querySelector('.arrow').classList.toggle('rotate-180')">
|
|
<h2 class="text-2xl font-bold">Auto-Routing Logic & Metadata</h2>
|
|
<svg class="arrow w-6 h-6 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg>
|
|
</div>
|
|
<button onclick="window.startCustomTour(MODELS_TOUR, 'tour_models_v1', true, true)" class="flex items-center gap-2 text-[10px] font-black uppercase text-indigo-400 hover:text-white transition-colors bg-white/5 px-3 py-1 rounded-lg">
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.79 4 4 0 2.21-1.79 4-4 4-1.742 0-3.223-.835-3.772-2M12 18.5v.01M12 5.5v.01"></path></svg>
|
|
Replay Tour
|
|
</button>
|
|
</div>
|
|
|
|
<div id="doc-body" class="mt-4 border-t border-white/5 pt-4">
|
|
<p class="text-current">
|
|
The Models Manager defines the "Intelligence DNA" for the virtual <code class="inline-code">auto</code> model.
|
|
Instead of managing server hardware here (which has moved to **Workflows**), this page focuses on tagging models so the Gateway knows how to route requests based on prompt content.
|
|
</p>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mt-6">
|
|
<div class="p-3 bg-white/5 rounded border border-white/10">
|
|
<span class="block text-indigo-400 font-bold text-xs uppercase mb-1">👁️ Supports Images</span>
|
|
<p class="text-[11px] text-gray-400 leading-relaxed">Required for vision tasks. If a user attaches an image, the 'auto' router filters for these models only.</p>
|
|
</div>
|
|
<div class="p-3 bg-white/5 rounded border border-white/10">
|
|
<span class="block text-amber-400 font-bold text-xs uppercase mb-1">🧠 Think (CoT)</span>
|
|
<p class="text-[11px] text-gray-400 leading-relaxed">Enable for models supporting internal reasoning (DeepSeek R1, etc). Prevents 'think' parameter stripping.</p>
|
|
</div>
|
|
<div class="p-3 bg-white/5 rounded border border-white/10">
|
|
<span class="block text-emerald-400 font-bold text-xs uppercase mb-1">💻 Code</span>
|
|
<p class="text-[11px] text-gray-400 leading-relaxed">Tagged as programming experts. Used when the router detects code keywords (e.g. 'def', 'class', 'import').</p>
|
|
</div>
|
|
<div class="p-3 bg-white/5 rounded border border-white/10">
|
|
<span class="block text-sky-400 font-bold text-xs uppercase mb-1">⚡ Fast</span>
|
|
<p class="text-[11px] text-gray-400 leading-relaxed">Small, optimized models. Used when 'fast_model' option is set in the request for low-latency tasks.</p>
|
|
</div>
|
|
<div class="p-3 bg-white/5 rounded border border-white/10">
|
|
<span class="block text-purple-400 font-bold text-xs uppercase mb-1">🧩 Reasoning</span>
|
|
<p class="text-[11px] text-gray-400 leading-relaxed">Complex logic models. Triggered by keywords like 'solve', 'math', 'why', or 'step by step'.</p>
|
|
</div>
|
|
<div class="p-3 bg-white/5 rounded border border-white/10">
|
|
<span class="block text-rose-400 font-bold text-xs uppercase mb-1">📏 Max Context</span>
|
|
<p class="text-[11px] text-gray-400 leading-relaxed">Token limit. The router checks if the current chat history fits before selecting the model.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<form id="metadata-form" action="{{ url_for('admin_update_model_metadata') }}" method="post" class="card-style flex flex-col h-[70vh]">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
|
|
|
<div class="flex flex-col md:flex-row justify-between items-start md:items-center gap-4 mb-6 flex-shrink-0">
|
|
<h2 class="text-2xl font-bold">Configure Model Capabilities</h2>
|
|
|
|
<div class="flex items-center gap-4 w-full md:w-auto">
|
|
<!-- Search Bar -->
|
|
<div class="relative flex-grow md:w-80">
|
|
<span class="absolute inset-y-0 left-0 pl-3 flex items-center text-gray-500">
|
|
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg>
|
|
</span>
|
|
<input type="text" id="model-search" onkeyup="filterModels()" placeholder="Search names, tags, servers..." class="w-full pl-10 pr-4 py-2 bg-black/40 border border-white/10 rounded-xl text-sm focus:border-indigo-500 outline-none transition-all">
|
|
</div>
|
|
|
|
<button type="button" onclick="window.location.reload()" class="p-2 bg-white/5 hover:bg-white/10 rounded-xl text-indigo-400 transition-colors" title="Refresh list from Database">
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path></svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex-grow overflow-auto border border-white/5 rounded-xl bg-black/20 custom-scrollbar">
|
|
<table class="min-w-full" id="models-table">
|
|
<thead class="sticky top-0 bg-[#0f0f15] z-10 shadow-md">
|
|
<tr>
|
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Model Name</th>
|
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Servers</th>
|
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Description</th>
|
|
<th class="px-4 py-3 text-center text-xs font-medium text-gray-400 uppercase tracking-wider">
|
|
<div class="mb-1">Embedding</div>
|
|
<div class="flex justify-center gap-2 text-[9px] font-bold">
|
|
<button type="button" onclick="toggleColumn('is_embedding_model_', true)" class="text-indigo-400 hover:text-white uppercase">All</button>
|
|
<span class="text-gray-600">|</span>
|
|
<button type="button" onclick="toggleColumn('is_embedding_model_', false)" class="text-gray-500 hover:text-white uppercase">None</button>
|
|
</div>
|
|
</th>
|
|
<th class="px-4 py-3 text-center text-xs font-medium text-gray-400 uppercase tracking-wider">
|
|
<div class="mb-1">Supports Images</div>
|
|
<div class="flex justify-center gap-2 text-[9px] font-bold">
|
|
<button type="button" onclick="toggleColumn('supports_images_', true)" class="text-indigo-400 hover:text-white uppercase">All</button>
|
|
<span class="text-gray-600">|</span>
|
|
<button type="button" onclick="toggleColumn('supports_images_', false)" class="text-gray-500 hover:text-white uppercase">None</button>
|
|
</div>
|
|
</th>
|
|
<th class="px-4 py-3 text-center text-xs font-medium text-gray-400 uppercase tracking-wider">
|
|
<div class="mb-1">Think (CoT)</div>
|
|
<div class="flex justify-center gap-2 text-[9px] font-bold">
|
|
<button type="button" onclick="toggleColumn('supports_thinking_', true)" class="text-indigo-400 hover:text-white uppercase">All</button>
|
|
<span class="text-gray-600">|</span>
|
|
<button type="button" onclick="toggleColumn('supports_thinking_', false)" class="text-gray-500 hover:text-white uppercase">None</button>
|
|
</div>
|
|
</th>
|
|
<th class="px-4 py-3 text-center text-xs font-medium text-gray-400 uppercase tracking-wider">
|
|
<div class="mb-1">Code</div>
|
|
<div class="flex justify-center gap-2 text-[9px] font-bold">
|
|
<button type="button" onclick="toggleColumn('is_code_model_', true)" class="text-indigo-400 hover:text-white uppercase">All</button>
|
|
<span class="text-gray-600">|</span>
|
|
<button type="button" onclick="toggleColumn('is_code_model_', false)" class="text-gray-500 hover:text-white uppercase">None</button>
|
|
</div>
|
|
</th>
|
|
<th class="px-4 py-3 text-center text-xs font-medium text-gray-400 uppercase tracking-wider">
|
|
<div class="mb-1">Fast</div>
|
|
<div class="flex justify-center gap-2 text-[9px] font-bold">
|
|
<button type="button" onclick="toggleColumn('is_fast_model_', true)" class="text-indigo-400 hover:text-white uppercase">All</button>
|
|
<span class="text-gray-600">|</span>
|
|
<button type="button" onclick="toggleColumn('is_fast_model_', false)" class="text-gray-500 hover:text-white uppercase">None</button>
|
|
</div>
|
|
</th>
|
|
<th class="px-4 py-3 text-center text-xs font-medium text-gray-400 uppercase tracking-wider">
|
|
<div class="mb-1">Reasoning</div>
|
|
<div class="flex justify-center gap-2 text-[9px] font-bold">
|
|
<button type="button" onclick="toggleColumn('is_reasoning_model_', true)" class="text-indigo-400 hover:text-white uppercase">All</button>
|
|
<span class="text-gray-600">|</span>
|
|
<button type="button" onclick="toggleColumn('is_reasoning_model_', false)" class="text-gray-500 hover:text-white uppercase">None</button>
|
|
</div>
|
|
</th>
|
|
<th class="px-4 py-3 text-center text-xs font-medium text-gray-400 uppercase tracking-wider">Max Context</th>
|
|
<th class="px-4 py-3 text-center text-xs font-medium text-gray-400 uppercase tracking-wider">Size (B)</th>
|
|
<th class="px-4 py-3 text-center text-xs font-medium text-gray-400 uppercase tracking-wider">Scale</th>
|
|
<th class="px-4 py-3 text-center text-xs font-medium text-gray-400 uppercase tracking-wider">Priority</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-white/10" id="metadata-tbody">
|
|
{% for meta in metadata_list %}
|
|
<tr class="align-top metadata-row">
|
|
<td class="px-4 py-4 whitespace-nowrap text-sm font-medium text-current font-mono">{{ meta.model_name }}</td>
|
|
<td class="px-4 py-4 whitespace-nowrap text-xs">
|
|
<div class="flex flex-wrap gap-1">
|
|
{% for srv in meta.origin_servers %}
|
|
<button type="button"
|
|
onclick="toggleModelOnServer(event, {{ srv.id }}, '{{ meta.model_name }}')"
|
|
class="server-badge px-2 py-1 rounded-full border transition-all hover:scale-105 active:scale-95
|
|
{% if srv.is_allowed %}
|
|
bg-emerald-500/10 text-emerald-400 border-emerald-500/30
|
|
{% else %}
|
|
bg-red-500/10 text-red-400 border-red-500/30
|
|
{% endif %}"
|
|
title="{{ 'Active: Click to Deactivate' if srv.is_allowed else 'Blocked: Click to Activate' }} on {{ srv.name }}">
|
|
{{ srv.name }}
|
|
</button>
|
|
{% endfor %}
|
|
</div>
|
|
</td>
|
|
<td class="px-4 py-4 whitespace-nowrap">
|
|
<input type="text" name="description_{{ meta.id }}" value="{{ meta.description or '' }}" placeholder="A short description..." class="w-full min-w-48 px-2 py-1 rounded-md shadow-sm sm:text-sm">
|
|
</td>
|
|
<td class="px-4 py-4 whitespace-nowrap text-center">
|
|
<input type="checkbox" name="is_embedding_model_{{ meta.id }}" value="true" class="embedding-toggle h-5 w-5 rounded text-amber-500" data-meta-id="{{ meta.id }}" {% if meta.is_embedding_model %}checked{% endif %}>
|
|
</td>
|
|
<td class="px-4 py-4 whitespace-nowrap text-center">
|
|
<input type="checkbox" name="supports_images_{{ meta.id }}" value="true" class="capability-toggle h-5 w-5 rounded text-[var(--color-primary-600)]" {% if meta.supports_images %}checked{% endif %}>
|
|
</td>
|
|
<td class="px-4 py-4 whitespace-nowrap text-center">
|
|
<input type="checkbox" name="supports_images_{{ meta.id }}" value="true" class="capability-toggle h-5 w-5 rounded text-[var(--color-primary-600)]" data-meta-id="{{ meta.id }}" {% if meta.supports_images %}checked{% endif %}>
|
|
</td>
|
|
<td class="px-4 py-4 whitespace-nowrap text-center">
|
|
<input type="checkbox" name="supports_thinking_{{ meta.id }}" value="true" class="h-5 w-5 rounded text-[var(--color-primary-600)]" {% if meta.supports_thinking %}checked{% endif %}>
|
|
</td>
|
|
<td class="px-4 py-4 whitespace-nowrap text-center">
|
|
<input type="checkbox" name="is_code_model_{{ meta.id }}" value="true" class="h-5 w-5 rounded text-[var(--color-primary-600)]" {% if meta.is_code_model %}checked{% endif %}>
|
|
</td>
|
|
<td class="px-4 py-4 whitespace-nowrap text-center">
|
|
<input type="checkbox" name="is_fast_model_{{ meta.id }}" value="true" class="h-5 w-5 rounded text-[var(--color-primary-600)]" {% if meta.is_fast_model %}checked{% endif %}>
|
|
</td>
|
|
<td class="px-4 py-4 whitespace-nowrap text-center">
|
|
<input type="checkbox" name="is_reasoning_model_{{ meta.id }}" value="true" class="h-5 w-5 rounded text-[var(--color-primary-600)]" {% if meta.is_reasoning_model %}checked{% endif %}>
|
|
</td>
|
|
<td class="px-4 py-4 whitespace-nowrap text-center">
|
|
<div class="flex items-center justify-center gap-1">
|
|
<input type="number" id="ctx_input_{{ meta.id }}" name="max_context_{{ meta.id }}" value="{{ meta.max_context }}" step="1024" class="w-24 px-2 py-1 rounded-md shadow-sm sm:text-sm text-center">
|
|
<button type="button" onclick="refreshContext('{{ meta.model_name }}', 'ctx_input_{{ meta.id }}')" class="p-1.5 hover:bg-white/10 rounded-md text-gray-500 hover:text-indigo-400 transition-colors" title="Sync from server">
|
|
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path></svg>
|
|
</button>
|
|
</div>
|
|
</td>
|
|
<td class="px-4 py-4 whitespace-nowrap text-center">
|
|
<input type="number" step="0.1" name="model_size_{{ meta.id }}" value="{{ meta.model_size }}"
|
|
class="w-16 px-2 py-1 rounded-md shadow-sm sm:text-sm text-center {% if meta.model_size < 0 %}text-red-400 font-bold{% endif %}"
|
|
title="{{ 'Size Unknown - AI config or manual entry needed' if meta.model_size < 0 else 'Parameter count in Billions' }}">
|
|
</td>
|
|
<td class="px-4 py-4 whitespace-nowrap text-center">
|
|
<select name="model_scale_{{ meta.id }}" class="bg-black/40 border border-white/10 rounded p-1 text-[10px] font-bold uppercase">
|
|
<option value="1" {% if meta.model_scale == 1 %}selected{% endif %}>Small</option>
|
|
<option value="2" {% if meta.model_scale == 2 %}selected{% endif %}>Medium</option>
|
|
<option value="3" {% if meta.model_scale == 3 %}selected{% endif %}>Large</option>
|
|
</select>
|
|
</td>
|
|
<td class="px-4 py-4 whitespace-nowrap text-center">
|
|
<input type="number" name="priority_{{ meta.id }}" value="{{ meta.priority }}" class="w-16 px-2 py-1 rounded-md shadow-sm sm:text-sm text-center">
|
|
</td>
|
|
</tr>
|
|
{% else %}
|
|
<tr>
|
|
<td colspan="6" class="px-6 py-10 text-center text-gray-400">
|
|
No models found across any active servers. Go to the <a href="{{ url_for('admin_servers') }}" class="font-semibold text-[var(--color-primary-500)] hover:underline">Server Management</a> page and refresh your servers to discover models.
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="flex justify-end pt-6 border-t border-white/10 mt-6">
|
|
<button type="submit" class="w-full md:w-auto justify-center py-2 px-6 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-[var(--color-primary-600)] hover:bg-[var(--color-primary-700)] focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[var(--color-primary-500)]">
|
|
Save All Changes
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<script>
|
|
let activeServerFilters = new Set(['all']);
|
|
|
|
function toggleServerFilter(serverId, btn) {
|
|
if (serverId === 'all') {
|
|
activeServerFilters.clear();
|
|
activeServerFilters.add('all');
|
|
document.querySelectorAll('.server-filter-btn').forEach(b => {
|
|
b.className = "server-filter-btn px-4 py-2 rounded-xl text-[10px] font-black uppercase tracking-widest border border-white/5 bg-white/5 text-gray-500 hover:text-indigo-300 transition-all";
|
|
});
|
|
btn.className = "server-filter-btn px-4 py-2 rounded-xl text-[10px] font-black uppercase tracking-widest border border-indigo-500/30 bg-indigo-600 text-white shadow-lg shadow-indigo-900/20 transition-all";
|
|
} else {
|
|
// Remove 'all' if present
|
|
if (activeServerFilters.has('all')) {
|
|
activeServerFilters.delete('all');
|
|
const allBtn = document.querySelector('[data-server-id="all"]');
|
|
allBtn.className = "server-filter-btn px-4 py-2 rounded-xl text-[10px] font-black uppercase tracking-widest border border-white/5 bg-white/5 text-gray-500 hover:text-indigo-300 transition-all";
|
|
}
|
|
|
|
if (activeServerFilters.has(serverId)) {
|
|
activeServerFilters.delete(serverId);
|
|
btn.className = "server-filter-btn px-4 py-2 rounded-xl text-[10px] font-black uppercase tracking-widest border border-white/5 bg-white/5 text-gray-500 hover:text-indigo-300 transition-all";
|
|
} else {
|
|
activeServerFilters.add(serverId);
|
|
btn.className = "server-filter-btn px-4 py-2 rounded-xl text-[10px] font-black uppercase tracking-widest border border-indigo-500/30 bg-indigo-600 text-white shadow-lg shadow-indigo-900/20 transition-all";
|
|
}
|
|
|
|
// If nothing selected, reset to all
|
|
if (activeServerFilters.size === 0) {
|
|
toggleServerFilter('all', document.querySelector('[data-server-id="all"]'));
|
|
return;
|
|
}
|
|
}
|
|
filterModels();
|
|
}
|
|
|
|
function clearServerFilters() {
|
|
toggleServerFilter('all', document.querySelector('[data-server-id="all"]'));
|
|
}
|
|
|
|
/**
|
|
* Filters the table rows based on search input and server selection.
|
|
*/
|
|
function filterModels() {
|
|
const input = document.getElementById('model-search');
|
|
const searchFilter = input.value.toLowerCase();
|
|
const rows = document.querySelectorAll('#metadata-tbody tr');
|
|
|
|
rows.forEach(row => {
|
|
const name = row.cells[0].textContent.toLowerCase();
|
|
const desc = row.querySelector('input[name^="description_"]').value.toLowerCase();
|
|
|
|
// Server matching
|
|
let serverMatch = activeServerFilters.has('all');
|
|
if (!serverMatch) {
|
|
// Get server IDs from badges in the second cell
|
|
const badges = row.cells[1].querySelectorAll('.server-badge');
|
|
badges.forEach(badge => {
|
|
// Extract ID from the onclick attribute: toggleModelOnServer(event, ID, 'name')
|
|
const match = badge.getAttribute('onclick').match(/toggleModelOnServer\(event,\s*(\d+),/);
|
|
if (match && activeServerFilters.has(parseInt(match[1]))) {
|
|
serverMatch = true;
|
|
}
|
|
});
|
|
}
|
|
|
|
// Capability search tags
|
|
const isVision = row.querySelector('input[name^="supports_images_"]').checked;
|
|
const isCode = row.querySelector('input[name^="is_code_model_"]').checked;
|
|
const isFast = row.querySelector('input[name^="is_fast_model_"]').checked;
|
|
const isReasoning = row.querySelector('input[name^="is_reasoning_model_"]').checked;
|
|
const tags = [];
|
|
if(isVision) tags.push("vision", "images");
|
|
if(isCode) tags.push("code", "programming");
|
|
if(isFast) tags.push("fast", "small");
|
|
if(isReasoning) tags.push("reasoning", "logic");
|
|
const tagStr = tags.join(" ");
|
|
|
|
const textMatch = name.includes(searchFilter) || desc.includes(searchFilter) || tagStr.includes(searchFilter);
|
|
|
|
if (serverMatch && textMatch) {
|
|
row.style.display = "";
|
|
} else {
|
|
row.style.display = "none";
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Calls the backend to fetch current context length for a model.
|
|
*/
|
|
async function refreshContext(modelName, inputId) {
|
|
const input = document.getElementById(inputId);
|
|
const btn = input.nextElementSibling;
|
|
|
|
// Visual feedback
|
|
btn.classList.add('animate-spin', 'text-indigo-400');
|
|
|
|
try {
|
|
const response = await fetch(`{{ url_for('admin_refresh_model_context') }}?model_name=${encodeURIComponent(modelName)}`);
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
input.value = data.context_length;
|
|
input.classList.add('ring-2', 'ring-green-500');
|
|
setTimeout(() => input.classList.remove('ring-2', 'ring-green-500'), 2000);
|
|
window.showToast(`Context for ${modelName} updated.`, 'success');
|
|
} else {
|
|
window.showErrorModal("Sync Failed", data);
|
|
}
|
|
} catch (e) {
|
|
window.showErrorModal("Connection Fault", { error: e.message });
|
|
} finally {
|
|
btn.classList.remove('animate-spin', 'text-indigo-400');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle Row Selection/Highlighting
|
|
*/
|
|
document.getElementById('metadata-tbody').addEventListener('click', (e) => {
|
|
const row = e.target.closest('.metadata-row');
|
|
if (!row) return;
|
|
|
|
// Optional: Clear other highlights if you want "Single Selection" mode
|
|
// document.querySelectorAll('.metadata-row').forEach(r => r.classList.remove('active-row'));
|
|
|
|
row.classList.toggle('active-row');
|
|
});
|
|
|
|
/**
|
|
* Toggles all checkboxes in a column based on the name prefix.
|
|
*/
|
|
function toggleColumn(prefix, value) {
|
|
const checkboxes = document.querySelectorAll(`input[name^="${prefix}"]`);
|
|
checkboxes.forEach(cb => {
|
|
cb.checked = value;
|
|
// Trigger change event to run the exclusivity logic
|
|
cb.dispatchEvent(new Event('change', { bubbles: true }));
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Mutual Exclusivity: If Embedding is selected, disable and clear other capabilities.
|
|
*/
|
|
document.addEventListener('change', function(e) {
|
|
if (e.target.classList.contains('embedding-toggle')) {
|
|
const row = e.target.closest('tr');
|
|
const otherCheckboxes = row.querySelectorAll('input[type="checkbox"]:not(.embedding-toggle)');
|
|
|
|
if (e.target.checked) {
|
|
otherCheckboxes.forEach(cb => {
|
|
cb.checked = false;
|
|
cb.disabled = true;
|
|
cb.parentElement.classList.add('opacity-30');
|
|
});
|
|
} else {
|
|
otherCheckboxes.forEach(cb => {
|
|
cb.disabled = false;
|
|
cb.parentElement.classList.remove('opacity-30');
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
// Run on load for existing state
|
|
document.querySelectorAll('.embedding-toggle').forEach(el => {
|
|
if (el.checked) el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
});
|
|
|
|
/**
|
|
* Toggles administrative allowed status of a model on a specific server via AJAX.
|
|
*/
|
|
async function toggleModelOnServer(event, serverId, modelName) {
|
|
const btn = event.currentTarget;
|
|
const originalClasses = btn.className;
|
|
|
|
// Visual feedback for processing
|
|
btn.classList.add('opacity-50', 'animate-pulse');
|
|
btn.disabled = true;
|
|
|
|
try {
|
|
const response = await fetch("{{ url_for('admin_toggle_model_server') }}", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"X-CSRF-Token": "{{ csrf_token }}"
|
|
},
|
|
body: JSON.stringify({
|
|
server_id: serverId,
|
|
model_name: modelName
|
|
})
|
|
});
|
|
|
|
const data = await response.json();
|
|
if (data.success) {
|
|
// Update styling based on new state
|
|
if (data.new_state) {
|
|
btn.className = "server-badge px-2 py-1 rounded-full border transition-all hover:scale-105 active:scale-95 bg-emerald-500/10 text-emerald-400 border-emerald-500/30";
|
|
btn.title = "Active: Click to Deactivate on " + btn.innerText;
|
|
window.showToast(`${modelName} enabled on server.`, 'success');
|
|
} else {
|
|
btn.className = "server-badge px-2 py-1 rounded-full border transition-all hover:scale-105 active:scale-95 bg-red-500/10 text-red-400 border-red-500/30";
|
|
btn.title = "Blocked: Click to Activate on " + btn.innerText;
|
|
window.showToast(`${modelName} blocked on server.`, 'info');
|
|
}
|
|
} else {
|
|
window.showErrorModal("Toggle Failed", data);
|
|
btn.className = originalClasses;
|
|
}
|
|
} catch (e) {
|
|
window.showErrorModal("Network Fault", { error: e.message });
|
|
btn.className = originalClasses;
|
|
} finally {
|
|
btn.classList.remove('opacity-50', 'animate-pulse');
|
|
btn.disabled = false;
|
|
}
|
|
}
|
|
|
|
// --- Page Tour Logic ---
|
|
const MODELS_TOUR = [
|
|
{
|
|
target: '#metadata-tbody tr:first-child td:first-child',
|
|
text: "This is your <b>Hub Identity</b>. Every discovered model across your cluster is listed here uniquely."
|
|
},
|
|
{
|
|
target: '.capability-toggle',
|
|
text: "<b>Supports Images</b>: Vital for VLM routing. If a user attaches a file, the Hub only considers models with this checked."
|
|
},
|
|
{
|
|
target: 'input[name^="supports_thinking_"]',
|
|
text: "<b>Think (CoT)</b>: Enable this for Reasoning models. It prevents the Gateway from stripping the 'think' parameter."
|
|
},
|
|
{
|
|
target: 'input[name^="max_context_"]',
|
|
text: "<b>Max Context</b>: The token ceiling. The 'auto' router will bypass small models if the chat history is too long."
|
|
},
|
|
{
|
|
target: 'input[name^="priority_"]',
|
|
text: "<b>Priority</b>: Tie-breaker. If two models match the request, the one with the <b>lowest value</b> (e.g., 1 vs 10) wins."
|
|
},
|
|
{
|
|
target: '#btn-save-all',
|
|
text: "Don't forget to <b>Save All Changes</b> to update the cluster metadata."
|
|
}
|
|
];
|
|
|
|
// Ensure tour triggers after all components are settled
|
|
window.addEventListener('load', () => {
|
|
if (window.startCustomTour) {
|
|
window.startCustomTour(MODELS_TOUR, 'tour_models_v1', {{ settings.tour_models | tojson }});
|
|
}
|
|
});
|
|
|
|
let currentConfigTaskId = null;
|
|
|
|
window.stopAIConfig = async () => {
|
|
if (!currentConfigTaskId) return;
|
|
const resp = await fetch(`/admin/system/stop-task/${currentConfigTaskId}`, {
|
|
method: 'POST',
|
|
headers: { 'X-CSRF-Token': "{{ csrf_token }}" }
|
|
});
|
|
document.getElementById('ai-stop-btn').classList.add('hidden');
|
|
};
|
|
|
|
window.runAIConfig = async () => {
|
|
const btn = document.getElementById('ai-config-btn');
|
|
const stopBtn = document.getElementById('ai-stop-btn');
|
|
const console = document.getElementById('ai-config-console');
|
|
const url = document.getElementById('ai-config-url').value;
|
|
|
|
if(!url) return alert("Scorecard URL required.");
|
|
|
|
btn.disabled = true;
|
|
btn.innerText = "CONFIGURING...";
|
|
console.classList.remove('hidden');
|
|
console.innerHTML = '<div class="text-purple-400 font-bold">[Architect] Handshaking with cluster...</div>';
|
|
|
|
const formData = new FormData();
|
|
formData.append('url', url);
|
|
formData.append('csrf_token', "{{ csrf_token }}");
|
|
|
|
try {
|
|
const resp = await fetch("{{ url_for('admin_ai_configure_models') }}", {
|
|
method: 'POST',
|
|
body: formData
|
|
});
|
|
const data = await resp.json();
|
|
if(data.success) {
|
|
currentConfigTaskId = data.task_id;
|
|
stopBtn.classList.remove('hidden');
|
|
console.innerHTML += `<div class="text-indigo-400">[System] Task ${data.task_id} shifted to background. You can navigate away.</div>`;
|
|
} else {
|
|
console.innerHTML += `<div class="text-red-400">[Error] ${data.error}</div>`;
|
|
btn.disabled = false;
|
|
btn.innerText = "Sync Metadata";
|
|
}
|
|
} catch(e) {
|
|
console.innerHTML += `<div class="text-red-500">[Network Error] ${e.message}</div>`;
|
|
btn.disabled = false;
|
|
}
|
|
};
|
|
|
|
// SSE Listener for Config Trace & Progress
|
|
window.showNotification = (text) => {
|
|
const toast = document.getElementById('global-notification');
|
|
document.getElementById('notif-text').innerText = text;
|
|
toast.classList.remove('translate-x-full');
|
|
setTimeout(hideNotification, 6000);
|
|
};
|
|
|
|
window.hideNotification = () => {
|
|
document.getElementById('global-notification').classList.add('translate-x-full');
|
|
};
|
|
|
|
const configSource = new EventSource("{{ url_for('sse_events') }}");
|
|
configSource.onmessage = (e) => {
|
|
const msg = JSON.parse(e.data);
|
|
if (msg.type === 'completed' && msg.id.startsWith('sys_')) {
|
|
window.showNotification(msg.error || "System background task finished.");
|
|
}
|
|
if(msg.id && msg.id.startsWith('sys_build_config_')) {
|
|
const consoleEl = document.getElementById('ai-config-console');
|
|
const progressContainer = document.getElementById('ai-config-progress-container');
|
|
const progressBar = document.getElementById('ai-progress-bar');
|
|
const progressPercent = document.getElementById('ai-progress-percent');
|
|
|
|
progressContainer.classList.remove('hidden');
|
|
|
|
// Update Progress Bar using the 'tokens' field mapped in Python
|
|
if (msg.tokens !== undefined) {
|
|
const pct = msg.tokens + '%';
|
|
progressBar.style.width = pct;
|
|
progressPercent.innerText = pct;
|
|
}
|
|
|
|
const color = msg.type === 'error' ? 'text-red-400' : 'text-purple-300';
|
|
consoleEl.innerHTML += `<div class="${color} opacity-80">${msg.error || 'Analyzing node data...'}</div>`;
|
|
consoleEl.scrollTop = consoleEl.scrollHeight;
|
|
|
|
if (msg.type === 'completed') {
|
|
progressBar.style.width = '100%';
|
|
progressPercent.innerText = '100%';
|
|
}
|
|
}
|
|
};
|
|
</script>
|
|
{% endblock %} |