Commit b92c6b3d authored by Your Name's avatar Your Name

feat: align user dashboard templates with admin, separate user/admin config visibility

- Make user_rotations.html and user_autoselects.html identical to admin templates
- Remove global config fallback for database users in handlers
- Separate provider/rotation visibility: users only see their own, admin only global
- Update version to 0.99.38
parent 2b07302d
......@@ -167,3 +167,4 @@ Thumbs.db
# Worktrees
.worktrees/
docs/superpowers/
......@@ -54,7 +54,7 @@ from .auth.qwen import QwenOAuth2
from .handlers import RequestHandler, RotationHandler, AutoselectHandler
from .utils import count_messages_tokens, split_messages_into_chunks, get_max_request_tokens_for_model
__version__ = "0.99.36"
__version__ = "0.99.38"
__all__ = [
# Config
"config",
......
......@@ -1971,10 +1971,15 @@ class RotationHandler:
# Load user-specific configs if user_id is provided
if user_id:
self._load_user_configs()
# Override config to only use user-specific configs with NO global fallback
self.rotations = {}
for rotation in self.user_rotations:
self.rotations[rotation['rotation_id']] = rotation['config']
else:
self.user_providers = {}
self.user_rotations = {}
self.user_autoselects = {}
self.rotations = self.config.rotations if hasattr(self.config, 'rotations') else {}
def _load_user_configs(self):
"""Load user-specific configurations from database"""
......@@ -1988,16 +1993,10 @@ class RotationHandler:
"""Reload user-specific configurations from database"""
if self.user_id:
self._load_user_configs()
def reload_user_configs(self):
"""Reload user-specific configurations from database"""
if self.user_id:
self._load_user_configs()
def reload_user_configs(self):
"""Reload user-specific configurations from database"""
if self.user_id:
self._load_user_configs()
# Refresh rotations dict after reload
self.rotations = {}
for rotation in self.user_rotations:
self.rotations[rotation['rotation_id']] = rotation['config']
def _get_provider_type(self, provider_id: str) -> str:
"""Get the provider type from configuration"""
......@@ -2101,7 +2100,9 @@ class RotationHandler:
model_id = model_config.get('model_id') or model_config.get('name') or model_config.get('id', '')
# Try to get defaults from the referenced rotation (first model in the rotation)
if model_id in self.config.rotations:
if self.user_id and model_id in self.rotations:
rotation_config = self.rotations[model_id]
elif model_id in self.config.rotations:
rotation_config = self.config.rotations[model_id]
# Check each default field
......@@ -2332,10 +2333,16 @@ class RotationHandler:
logger.info(f"User ID: {self.user_id}")
# Check for user-specific rotation config first
if self.user_id and rotation_id in self.user_rotations:
rotation_config = self.user_rotations[rotation_id]
if self.user_id:
# Database user: ONLY use user-specific configs - NO global fallback
rotation_config = next((rot['config'] for rot in self.user_rotations if rot['rotation_id'] == rotation_id), None)
if rotation_config:
logger.info(f"Using user-specific rotation config for {rotation_id}")
else:
logger.error(f"User rotation {rotation_id} not found - NO global fallback")
raise HTTPException(status_code=400, detail=f"Rotation {rotation_id} not found for this user")
else:
# Admin user: use global config
rotation_config = self.config.get_rotation(rotation_id)
logger.info(f"Using global rotation config for {rotation_id}")
......@@ -3857,10 +3864,15 @@ class AutoselectHandler:
# Load user-specific configs if user_id is provided
if user_id:
self._load_user_configs()
# Override config to only use user-specific configs with NO global fallback
self.autoselects = {}
for autoselect in self.user_autoselects:
self.autoselects[autoselect['autoselect_id']] = autoselect['config']
else:
self.user_providers = {}
self.user_rotations = {}
self.user_autoselects = {}
self.autoselects = self.config.autoselect if hasattr(self.config, 'autoselect') else {}
def _load_user_configs(self):
"""Load user-specific configurations from database"""
......@@ -3870,6 +3882,15 @@ class AutoselectHandler:
self.user_rotations = db.get_user_rotations(self.user_id)
self.user_autoselects = db.get_user_autoselects(self.user_id)
def reload_user_configs(self):
"""Reload user-specific configurations from database"""
if self.user_id:
self._load_user_configs()
# Refresh autoselects dict after reload
self.autoselects = {}
for autoselect in self.user_autoselects:
self.autoselects[autoselect['autoselect_id']] = autoselect['config']
def _get_skill_file_content(self) -> str:
"""Load the autoselect.md skill file content"""
if self._skill_file_content is None:
......@@ -4158,9 +4179,9 @@ class AutoselectHandler:
return model_id
# Check if it's a rotation
elif selection_model in self.config.rotations:
elif (self.user_id and selection_model in self.rotations) or selection_model in self.config.rotations:
logger.info(f"Selection model '{selection_model}' is a rotation")
rotation_handler = RotationHandler()
rotation_handler = RotationHandler(user_id=self.user_id)
response = await rotation_handler.handle_rotation_request(selection_model, selection_request)
# Check if it's a provider/model format (e.g., "gemini/gemini-pro")
elif '/' in selection_model:
......@@ -4255,10 +4276,16 @@ class AutoselectHandler:
logger.warning(f"Response cache check failed: {cache_error}")
# Check for user-specific autoselect config first
if self.user_id and autoselect_id in self.user_autoselects:
autoselect_config = self.user_autoselects[autoselect_id]
if self.user_id:
# Database user: ONLY use user-specific configs - NO global fallback
autoselect_config = next((aut['config'] for aut in self.user_autoselects if aut['autoselect_id'] == autoselect_id), None)
if autoselect_config:
logger.info(f"Using user-specific autoselect config for {autoselect_id}")
else:
logger.error(f"User autoselect {autoselect_id} not found - NO global fallback")
raise HTTPException(status_code=400, detail=f"Autoselect {autoselect_id} not found for this user")
else:
# Admin user: use global config
autoselect_config = self.config.get_autoselect(autoselect_id)
logger.info(f"Using global autoselect config for {autoselect_id}")
......@@ -4560,9 +4587,9 @@ class AutoselectHandler:
request_data['stream'] = True
# Check if it's a rotation first
if selected_model_id in self.config.rotations:
if (self.user_id and selected_model_id in self.rotations) or selected_model_id in self.config.rotations:
logger.info(f"Proxying streaming request to rotation: {selected_model_id}")
rotation_handler = RotationHandler()
rotation_handler = RotationHandler(user_id=self.user_id)
response = await rotation_handler.handle_rotation_request(selected_model_id, request_data)
# Check if it's a provider/model format (e.g., "gemini/gemini-pro")
elif '/' in selected_model_id:
......
......@@ -4198,8 +4198,16 @@ async def dashboard_rotations(request: Request):
for rotation in user_rotations:
rotations_data["rotations"][rotation['rotation_id']] = rotation['config']
# Get available providers
# Get available providers - user-specific for database users
if is_config_admin:
# Admin: use global providers
available_providers = list(config.providers.keys()) if config else []
else:
# Database user: use ONLY their own providers
from aisbf.database import get_database
db = DatabaseRegistry.get_config_database()
user_providers = db.get_user_providers(current_user_id)
available_providers = [p['provider_id'] for p in user_providers]
# Check for success parameter
success = request.query_params.get('success')
......@@ -4218,7 +4226,7 @@ async def dashboard_rotations(request: Request):
}
)
else:
# Database user: use user template with proper context
# Database user: use user template
return templates.TemplateResponse(
request=request,
name="dashboard/user_rotations.html",
......@@ -4226,9 +4234,8 @@ async def dashboard_rotations(request: Request):
"request": request,
"session": request.session,
"__version__": __version__,
"user_rotations_json": json.dumps(rotations_data),
"rotations_json": json.dumps(rotations_data),
"available_providers": json.dumps(available_providers),
"user_id": current_user_id,
"success": "Configuration saved successfully!" if success else None
}
)
......@@ -4399,13 +4406,15 @@ async def dashboard_autoselect(request: Request):
for autoselect in user_autoselects:
autoselect_data[autoselect['autoselect_id']] = autoselect['config']
# Get available rotations
available_rotations = list(config.rotations.keys()) if config else []
# Check for success parameter
success = request.query_params.get('success')
# Get available provider models
if is_config_admin:
# Admin: use global rotations and providers
available_rotations = list(config.rotations.keys()) if config else []
available_models = []
# Add rotation IDs
# Add global rotation IDs
for rotation_id in available_rotations:
available_models.append({
'id': rotation_id,
......@@ -4413,7 +4422,7 @@ async def dashboard_autoselect(request: Request):
'type': 'rotation'
})
# Add provider models
# Add global provider models
providers_path = Path.home() / '.aisbf' / 'providers.json'
if not providers_path.exists():
providers_path = Path(__file__).parent / 'config' / 'providers.json'
......@@ -4433,10 +4442,6 @@ async def dashboard_autoselect(request: Request):
'type': 'provider'
})
# Check for success parameter
success = request.query_params.get('success')
if is_config_admin:
# Config admin: use admin template
return templates.TemplateResponse(
request=request,
......@@ -4451,16 +4456,16 @@ async def dashboard_autoselect(request: Request):
}
)
else:
# Database user: use user template with proper context
# Database user: use ONLY their own rotations and providers
from aisbf.database import get_database
db = DatabaseRegistry.get_config_database()
user_autoselects = db.get_user_autoselects(current_user_id)
# For database users, get available user rotations
# Get only user's own rotations
user_rotations = db.get_user_rotations(current_user_id)
available_rotations = [rot['rotation_id'] for rot in user_rotations]
# For database users, get available user providers
# Get only user's own providers
user_providers = db.get_user_providers(current_user_id)
available_models = []
......@@ -4484,6 +4489,7 @@ async def dashboard_autoselect(request: Request):
'type': 'provider'
})
# Database user: use user template
return templates.TemplateResponse(
request=request,
name="dashboard/user_autoselects.html",
......@@ -4491,7 +4497,7 @@ async def dashboard_autoselect(request: Request):
"request": request,
"session": request.session,
"__version__": __version__,
"user_autoselects_json": json.dumps(autoselect_data),
"autoselect_json": json.dumps(autoselect_data),
"available_rotations": json.dumps(available_rotations),
"available_models": json.dumps(available_models),
"user_id": current_user_id,
......
......@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "aisbf"
version = "0.99.36"
version = "0.99.38"
description = "AISBF - AI Service Broker Framework || AI Should Be Free - A modular proxy server for managing multiple AI provider integrations"
readme = "README.md"
license = "GPL-3.0-or-later"
......
......@@ -49,7 +49,7 @@ class InstallCommand(_install):
setup(
name="aisbf",
version="0.99.36",
version="0.99.38",
author="AISBF Contributors",
author_email="stefy@nexlab.net",
description="AISBF - AI Service Broker Framework || AI Should Be Free - A modular proxy server for managing multiple AI provider integrations",
......
<!--
Copyright (C) 2026 Stefy Lanza <stefy@nexlab.net>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
{% extends "base.html" %}
{% block title %}My Autoselects - AISBF{% endblock %}
{% block content %}
<div class="container">
<h1>My Autoselects</h1>
<p>Manage your personal autoselect configurations</p>
{% if success %}
<div class="alert alert-success">{{ success }}</div>
{% endif %}
{% if error %}
<div class="alert alert-error">{{ error }}</div>
{% endif %}
<div class="card">
<h2>Autoselect Configurations</h2>
<div id="autoselects-list">
<!-- Autoselects will be loaded here -->
</div>
<button class="btn btn-primary" onclick="showAddAutoselectModal()">Add New Autoselect</button>
<button class="btn btn-success" onclick="applyChanges()" style="margin-left: 10px;">✓ Apply Changes</button>
</div>
<h2 style="margin-bottom: 30px;">My Autoselect Configuration</h2>
{% if success %}
<div class="alert alert-success">{{ success }}</div>
{% endif %}
{% if error %}
<div class="alert alert-error">{{ error }}</div>
{% endif %}
<div id="autoselect-list" style="margin-bottom: 20px;">
<!-- Autoselect list will be rendered here -->
</div>
<!-- Modal for adding/editing autoselects -->
<div id="autoselect-modal" class="modal" style="display: none;">
<div class="modal-content">
<div class="modal-header">
<h3 id="modal-title">Add Autoselect</h3>
<button class="close-btn" onclick="closeModal()">&times;</button>
</div>
<div class="modal-body">
<form id="autoselect-form">
<div class="form-group">
<label for="autoselect-name">Autoselect Name:</label>
<input type="text" id="autoselect-name" required>
</div>
<div class="form-group">
<label for="autoselect-config">Autoselect Configuration (JSON):</label>
<textarea id="autoselect-config" rows="20" placeholder='{"model_name": "autoselect-name", "description": "My autoselect config", "fallback": "provider/model"}'></textarea>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Save</button>
<button type="button" class="btn btn-secondary" onclick="closeModal()">Cancel</button>
</div>
</form>
</div>
</div>
<button type="button" class="btn" onclick="addAutoselect()" style="margin-top: 20px;">Add Autoselect</button>
<div style="display: flex; gap: 10px; margin-top: 20px;">
<button type="button" class="btn" onclick="saveAutoselect()">Save Configuration</button>
<a href="{{ url_for(request, '/dashboard') }}" class="btn btn-secondary">Cancel</a>
</div>
<script>
let autoselects = {{ user_autoselects_json | safe }};
let currentEditingIndex = -1;
function renderAutoselects() {
const container = document.getElementById('autoselects-list');
const autoselectData = {
config: {{ autoselect_json | safe }},
rotations: {{ available_rotations | safe }},
models: {{ available_models | safe }},
saveUrl: "{{ url_for(request, '/dashboard/autoselect') }}",
successUrl: "{{ url_for(request, '/dashboard/autoselect?success=1') }}"
};
let autoselectConfig = autoselectData.config;
let availableRotations = autoselectData.rotations;
let availableModels = autoselectData.models;
let expandedAutoselects = new Set();
function renderAutoselectList() {
const container = document.getElementById('autoselect-list');
container.innerHTML = '';
if (autoselects.length === 0) {
container.innerHTML = '<p class="empty-state">No autoselects configured yet. Click "Add New Autoselect" to get started.</p>';
if (!autoselectConfig || Object.keys(autoselectConfig).length === 0) {
container.innerHTML = '<p style="color: #a0a0a0;">No autoselect configurations defined</p>';
return;
}
autoselects.forEach((autoselect, index) => {
const div = document.createElement('div');
div.className = 'autoselect-item';
div.innerHTML = `
<div class="autoselect-header">
<h3>${autoselect.autoselect_id}</h3>
<div class="autoselect-actions">
<button class="btn btn-secondary btn-sm" onclick="editAutoselect(${index})">Edit</button>
<button class="btn btn-danger btn-sm" onclick="deleteAutoselect('${autoselect.autoselect_id}')">Delete</button>
Object.entries(autoselectConfig).forEach(([key, autoselect]) => {
const autoselectItem = document.createElement('div');
autoselectItem.className = 'autoselect-item';
autoselectItem.style.cssText = 'border: 1px solid #0f3460; margin-bottom: 10px; border-radius: 5px; background: #1a1a2e;';
const isExpanded = expandedAutoselects.has(key);
const modelCount = autoselect.available_models ? autoselect.available_models.length : 0;
autoselectItem.innerHTML = `
<div class="autoselect-header" onclick="toggleAutoselect('${key}')" style="padding: 15px; cursor: pointer; display: flex; justify-content: space-between; align-items: center; user-select: none;">
<div style="display: flex; align-items: center; gap: 10px;">
<span style="font-size: 18px;">${isExpanded ? '▼' : '▶'}</span>
<strong style="font-size: 16px;">${autoselect.model_name || key}</strong>
<span style="color: #a0a0a0; font-size: 14px;">(${modelCount} available model${modelCount !== 1 ? 's' : ''})</span>
</div>
<button type="button" class="btn btn-secondary" onclick="event.stopPropagation(); removeAutoselect('${key}')" style="background: #dc3545; padding: 5px 15px;">Remove</button>
</div>
<div class="autoselect-details">
<p><strong>Created:</strong> ${new Date(autoselect.created_at).toLocaleString()}</p>
<p><strong>Last Updated:</strong> ${new Date(autoselect.updated_at).toLocaleString()}</p>
<details>
<summary>Configuration (JSON)</summary>
<pre>${JSON.stringify(autoselect.config, null, 2)}</pre>
</details>
<div id="autoselect-details-${key}" style="display: ${isExpanded ? 'block' : 'none'}; padding: 20px; border-top: 1px solid #0f3460; background: #16213e;">
<!-- Details will be rendered here -->
</div>
`;
container.appendChild(div);
container.appendChild(autoselectItem);
if (isExpanded) {
renderAutoselectDetails(key);
}
});
}
function showAddAutoselectModal() {
currentEditingIndex = -1;
document.getElementById('modal-title').textContent = 'Add Autoselect';
document.getElementById('autoselect-name').value = '';
document.getElementById('autoselect-config').value = '{"model_name": "my-autoselect", "description": "My autoselect configuration", "fallback": "provider/model"}';
document.getElementById('autoselect-modal').style.display = 'block';
function toggleAutoselect(key) {
if (expandedAutoselects.has(key)) {
expandedAutoselects.delete(key);
} else {
expandedAutoselects.add(key);
}
renderAutoselectList();
}
function editAutoselect(index) {
currentEditingIndex = index;
const autoselect = autoselects[index];
document.getElementById('modal-title').textContent = 'Edit Autoselect';
document.getElementById('autoselect-name').value = autoselect.autoselect_id;
document.getElementById('autoselect-name').readOnly = true;
document.getElementById('autoselect-config').value = JSON.stringify(autoselect.config, null, 2);
document.getElementById('autoselect-modal').style.display = 'block';
}
function renderAutoselectDetails(autoselectKey) {
const container = document.getElementById(`autoselect-details-${autoselectKey}`);
const autoselect = autoselectConfig[autoselectKey];
// Default to "internal" if selection_model is not set
const selectionValue = autoselect.selection_model || 'internal';
// Build selection model options: internal + rotations + provider models
let selectionOptions = `<option value="internal" ${selectionValue === 'internal' ? 'selected' : ''}>internal (Use configured internal model)</option>`;
selectionOptions += '<optgroup label="Rotations">';
selectionOptions += availableRotations.map(r =>
`<option value="${r}" ${selectionValue === r ? 'selected' : ''}>${r}</option>`
).join('');
selectionOptions += '</optgroup>';
selectionOptions += '<optgroup label="Provider Models">';
selectionOptions += availableModels.map(m =>
`<option value="${m.id}" ${selectionValue === m.id ? 'selected' : ''}>${m.name}</option>`
).join('');
selectionOptions += '</optgroup>';
// Build fallback options: rotations + provider models
let fallbackOptions = '<optgroup label="Rotations">';
fallbackOptions += availableRotations.map(r =>
`<option value="${r}" ${autoselect.fallback === r ? 'selected' : ''}>${r}</option>`
).join('');
fallbackOptions += '</optgroup>';
fallbackOptions += '<optgroup label="Provider Models">';
fallbackOptions += availableModels.map(m =>
`<option value="${m.id}" ${autoselect.fallback === m.id ? 'selected' : ''}>${m.name}</option>`
).join('');
fallbackOptions += '</optgroup>';
container.innerHTML = `
<div class="form-group">
<label>Model Name</label>
<input type="text" value="${autoselect.model_name}" onchange="updateAutoselect('${autoselectKey}', 'model_name', this.value)" required>
</div>
function closeModal() {
document.getElementById('autoselect-modal').style.display = 'none';
document.getElementById('autoselect-name').readOnly = false;
currentEditingIndex = -1;
}
<div class="form-group">
<label>Capabilities (comma-separated)</label>
<input type="text" value="${autoselect.capabilities ? autoselect.capabilities.join(', ') : ''}" onchange="updateAutoselectCapabilities('${autoselectKey}', this.value)" placeholder="e.g., t2t, reasoning, multimodal">
</div>
function deleteAutoselect(autoselectName) {
if (!confirm(`Are you sure you want to delete the autoselect "${autoselectName}"? This action cannot be undone.`)) return;
<div class="form-group">
<label>
<input type="checkbox" ${autoselect.nsfw ? 'checked' : ''} onchange="updateAutoselect('${autoselectKey}', 'nsfw', this.checked)">
NSFW
</label>
</div>
fetch('{{ url_for(request, "/dashboard/user/autoselects") }}/' + encodeURIComponent(autoselectName), {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
}
}).then(response => {
if (response.ok) {
location.reload();
} else {
return response.json().then(data => {
throw new Error(data.error || 'Failed to delete autoselect');
});
}
}).catch(error => {
alert('Error: ' + error.message);
});
}
<div class="form-group">
<label>
<input type="checkbox" ${autoselect.privacy ? 'checked' : ''} onchange="updateAutoselect('${autoselectKey}', 'privacy', this.checked)">
Privacy
</label>
</div>
// Handle form submission
document.getElementById('autoselect-form').addEventListener('submit', function(e) {
e.preventDefault();
<div class="form-group">
<label>
<input type="checkbox" ${autoselect.classify_nsfw ? 'checked' : ''} onchange="updateAutoselect('${autoselectKey}', 'classify_nsfw', this.checked)">
Classify NSFW
</label>
<small style="color: #a0a0a0; font-size: 12px; display: block; margin-top: 5px;">Override global classify_nsfw setting for this autoselection</small>
</div>
const autoselectName = document.getElementById('autoselect-name').value.trim();
const configText = document.getElementById('autoselect-config').value.trim();
<div class="form-group">
<label>
<input type="checkbox" ${autoselect.classify_privacy ? 'checked' : ''} onchange="updateAutoselect('${autoselectKey}', 'classify_privacy', this.checked)">
Classify Privacy
</label>
<small style="color: #a0a0a0; font-size: 12px; display: block; margin-top: 5px;">Override global classify_privacy setting for this autoselection</small>
</div>
if (!autoselectName) {
alert('Autoselect name is required');
return;
}
<div class="form-group">
<label>
<input type="checkbox" ${autoselect.classify_semantic ? 'checked' : ''} onchange="updateAutoselect('${autoselectKey}', 'classify_semantic', this.checked)">
Classify Semantic
</label>
<small style="color: #a0a0a0; font-size: 12px; display: block; margin-top: 5px;">Override global classify_semantic setting for this autoselection</small>
</div>
if (!configText) {
alert('Autoselect configuration is required');
return;
}
<div class="form-group">
<label>Description</label>
<textarea onchange="updateAutoselect('${autoselectKey}', 'description', this.value)" style="min-height: 60px;">${autoselect.description || ''}</textarea>
</div>
let configObj;
try {
configObj = JSON.parse(configText);
} catch (e) {
alert('Invalid JSON configuration: ' + e.message);
<div class="form-group">
<label>Selection Model (Model to use for analysis)</label>
<select onchange="updateAutoselect('${autoselectKey}', 'selection_model', this.value)" required>
<option value="">Select model...</option>
${selectionOptions}
</select>
<small style="color: #a0a0a0; font-size: 12px; display: block; margin-top: 5px;">Choose "internal" to use the configured internal model, or select a rotation/provider model</small>
</div>
<div class="form-group">
<label>Fallback Model (Default if selection fails)</label>
<select onchange="updateAutoselect('${autoselectKey}', 'fallback', this.value)" required>
<option value="">Select model...</option>
${fallbackOptions}
</select>
<small style="color: #a0a0a0; font-size: 12px; display: block; margin-top: 5px;">Choose from rotations or provider models</small>
</div>
<h4 style="margin-top: 20px; margin-bottom: 10px;">Default Settings</h4>
<p style="color: #a0a0a0; font-size: 14px; margin-bottom: 10px;">Default values for models in this autoselect (optional - auto-derived from first model if not set)</p>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px;">
<div class="form-group">
<label>Default Rate Limit (seconds)</label>
<input type="number" value="${autoselect.default_rate_limit || ''}" onchange="updateAutoselect('${autoselectKey}', 'default_rate_limit', this.value ? parseFloat(this.value) : null)" step="0.1" placeholder="Optional">
</div>
<div class="form-group">
<label>Default Max Request Tokens</label>
<input type="number" value="${autoselect.default_max_request_tokens || ''}" onchange="updateAutoselect('${autoselectKey}', 'default_max_request_tokens', this.value ? parseInt(this.value) : null)" placeholder="Optional">
</div>
<div class="form-group">
<label>Default Context Size</label>
<input type="number" value="${autoselect.default_context_size || ''}" onchange="updateAutoselect('${autoselectKey}', 'default_context_size', this.value ? parseInt(this.value) : null)" placeholder="Optional">
</div>
<div class="form-group">
<label>Default Rate Limit TPM</label>
<input type="number" value="${autoselect.default_rate_limit_TPM || ''}" onchange="updateAutoselect('${autoselectKey}', 'default_rate_limit_TPM', this.value ? parseInt(this.value) : null)" placeholder="Optional">
<small style="color: #a0a0a0; font-size: 12px; display: block; margin-top: 5px;">Tokens per minute limit</small>
</div>
<div class="form-group">
<label>Default Rate Limit TPH</label>
<input type="number" value="${autoselect.default_rate_limit_TPH || ''}" onchange="updateAutoselect('${autoselectKey}', 'default_rate_limit_TPH', this.value ? parseInt(this.value) : null)" placeholder="Optional">
<small style="color: #a0a0a0; font-size: 12px; display: block; margin-top: 5px;">Tokens per hour limit</small>
</div>
<div class="form-group">
<label>Default Rate Limit TPD</label>
<input type="number" value="${autoselect.default_rate_limit_TPD || ''}" onchange="updateAutoselect('${autoselectKey}', 'default_rate_limit_TPD', this.value ? parseInt(this.value) : null)" placeholder="Optional">
<small style="color: #a0a0a0; font-size: 12px; display: block; margin-top: 5px;">Tokens per day limit</small>
</div>
<div class="form-group">
<label>Default Condense Context</label>
<input type="number" value="${autoselect.default_condense_context || ''}" onchange="updateAutoselect('${autoselectKey}', 'default_condense_context', this.value ? parseInt(this.value) : null)" placeholder="Optional">
<small style="color: #a0a0a0; font-size: 12px; display: block; margin-top: 5px;">Trigger context condensation at this token count</small>
</div>
</div>
<h4 style="margin-top: 20px; margin-bottom: 10px;">Available Models</h4>
<p style="color: #a0a0a0; font-size: 14px; margin-bottom: 10px;">Define which models can be selected and their descriptions for AI analysis</p>
<div id="models-${autoselectKey}"></div>
<button type="button" class="btn btn-secondary" onclick="addAutoselectModel('${autoselectKey}')" style="margin-top: 10px;">Add Model</button>
`;
renderAutoselectModels(autoselectKey);
}
function renderAutoselectModels(autoselectKey) {
const container = document.getElementById(`models-${autoselectKey}`);
const autoselect = autoselectConfig[autoselectKey];
if (!autoselect.available_models || autoselect.available_models.length === 0) {
container.innerHTML = '<p style="color: #a0a0a0;">No models configured</p>';
return;
}
const formData = new FormData();
formData.append('autoselect_name', autoselectName);
formData.append('autoselect_config', JSON.stringify(configObj));
container.innerHTML = '';
autoselect.available_models.forEach((model, index) => {
const modelDiv = document.createElement('div');
modelDiv.style.cssText = 'border: 1px solid #0f3460; padding: 15px; margin-bottom: 10px; border-radius: 3px; background: #1a1a2e;';
const modelOptions = availableModels.map(m =>
`<option value="${m.id}" ${model.model_id === m.id ? 'selected' : ''}>${m.name}</option>`
).join('');
modelDiv.innerHTML = `
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
<strong>Model ${index + 1}</strong>
<button type="button" class="btn btn-secondary" onclick="removeAutoselectModel('${autoselectKey}', ${index})" style="background: #dc3545; padding: 5px 10px; font-size: 12px;">Remove</button>
</div>
const url = '{{ url_for(request, "/dashboard/user/autoselects") }}';
const method = currentEditingIndex >= 0 ? 'PUT' : 'POST';
<div class="form-group">
<label>Model ID (Rotation or Provider Model)</label>
<select onchange="updateAutoselectModel('${autoselectKey}', ${index}, 'model_id', this.value)" required>
<option value="">Select model...</option>
${modelOptions}
</select>
<small style="color: #a0a0a0; font-size: 12px; display: block; margin-top: 5px;">Choose from rotations or provider models</small>
</div>
fetch(url, {
method: method,
body: formData
}).then(response => {
if (response.ok) {
closeModal();
location.reload();
} else {
return response.text().then(text => {
throw new Error(text || 'Failed to save autoselect');
});
}
}).catch(error => {
alert('Error: ' + error.message);
});
});
<div class="form-group">
<label>Description (Used by AI to select appropriate model)</label>
<textarea onchange="updateAutoselectModel('${autoselectKey}', ${index}, 'description', this.value)" style="min-height: 80px;" required>${model.description || ''}</textarea>
<small style="color: #666; font-size: 12px;">Be specific about when this model should be used (e.g., "Best for programming, code generation, debugging")</small>
</div>
async function applyChanges() {
const button = event.target;
const originalText = button.innerHTML;
<div class="form-group">
<label>
<input type="checkbox" ${model.nsfw ? 'checked' : ''} onchange="updateAutoselectModel('${autoselectKey}', ${index}, 'nsfw', this.checked)">
NSFW
</label>
</div>
try {
button.innerHTML = '🔄 Reloading...';
button.disabled = true;
<div class="form-group">
<label>
<input type="checkbox" ${model.privacy ? 'checked' : ''} onchange="updateAutoselectModel('${autoselectKey}', ${index}, 'privacy', this.checked)">
Privacy
</label>
</div>
`;
const response = await fetch('{{ url_for(request, "/dashboard/user/reload-config") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
container.appendChild(modelDiv);
});
}
if (response.ok) {
button.innerHTML = '✓ Applied Successfully!';
button.style.background = '#10b981';
setTimeout(() => {
button.innerHTML = originalText;
button.style.background = '';
button.disabled = false;
}, 2000);
} else {
const data = await response.json();
throw new Error(data.error || 'Failed to apply changes');
function addAutoselect() {
const key = prompt('Enter autoselect key (e.g., "autoselect", "smart-select"):');
if (!key) {
return;
}
} catch (error) {
button.innerHTML = '✗ Error';
button.style.background = '#ef4444';
setTimeout(() => {
button.innerHTML = originalText;
button.style.background = '';
button.disabled = false;
}, 2000);
alert('Error applying changes: ' + error.message);
if (autoselectConfig[key]) {
alert('Autoselect key already exists');
return;
}
}
renderAutoselects();
</script>
autoselectConfig[key] = {
model_name: key,
description: '',
selection_model: '',
fallback: '',
available_models: []
};
<style>
.autoselect-item {
background: #1a1a2e;
padding: 1rem;
margin: 1rem 0;
border-radius: 8px;
expandedAutoselects.add(key);
renderAutoselectList();
}
.autoselect-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
function removeAutoselect(key) {
if (confirm(`Remove autoselect "${key}"?`)) {
delete autoselectConfig[key];
expandedAutoselects.delete(key);
renderAutoselectList();
}
}
.autoselect-header h3 {
margin: 0;
function updateAutoselect(key, field, value) {
autoselectConfig[key][field] = value;
}
.autoselect-actions {
display: flex;
gap: 0.5rem;
}
function updateAutoselectCapabilities(autoselectKey, value) {
const trimmed = value.trim();
if (!trimmed) {
autoselectConfig[autoselectKey].capabilities = null;
return;
}
.autoselect-details p {
margin: 0.5rem 0;
// Split by comma and clean up
autoselectConfig[autoselectKey].capabilities =
trimmed.split(',').map(s => s.trim()).filter(s => s);
}
.autoselect-details pre {
background: #0f3460;
padding: 1rem;
border-radius: 4px;
overflow-x: auto;
white-space: pre-wrap;
word-wrap: break-word;
}
function addAutoselectModel(autoselectKey) {
if (!autoselectConfig[autoselectKey].available_models) {
autoselectConfig[autoselectKey].available_models = [];
}
.empty-state {
text-align: center;
color: #a0a0a0;
padding: 2rem;
autoselectConfig[autoselectKey].available_models.push({
model_id: '',
description: ''
});
renderAutoselectModels(autoselectKey);
}
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
function removeAutoselectModel(autoselectKey, index) {
if (confirm('Remove this model?')) {
autoselectConfig[autoselectKey].available_models.splice(index, 1);
renderAutoselectModels(autoselectKey);
}
}
.modal-content {
background: #1a1a2e;
border-radius: 8px;
width: 90%;
max-width: 600px;
max-height: 80vh;
overflow-y: auto;
function updateAutoselectModel(autoselectKey, index, field, value) {
autoselectConfig[autoselectKey].available_models[index][field] = value;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
border-bottom: 1px solid #0f3460;
async function saveAutoselect() {
try {
const response = await fetch(autoselectData.saveUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'config=' + encodeURIComponent(JSON.stringify(autoselectConfig, null, 2))
});
if (response.ok) {
window.location.href = autoselectData.successUrl;
} else {
alert('Error saving configuration');
}
} catch (error) {
alert('Error: ' + error.message);
}
}
.modal-header h3 {
margin: 0;
// Initial render
renderAutoselectList();
</script>
<style>
.autoselect-item {
animation: fadeIn 0.3s;
}
.close-btn {
background: none;
border: none;
color: #e0e0e0;
font-size: 1.5rem;
cursor: pointer;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
.autoselect-header:hover {
background: #0f3460;
}
.modal-body {
padding: 1rem;
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
.form-group {
margin-bottom: 1rem;
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
margin-bottom: 5px;
font-weight: 500;
color: #e0e0e0;
}
.form-group input[type="text"],
.form-group textarea {
.form-group textarea,
.form-group select {
width: 100%;
padding: 0.5rem;
border: 1px solid #0f3460;
border-radius: 4px;
background: #16213e;
color: #e0e0e0;
font-family: monospace;
padding: 8px;
border: 1px solid #ddd;
border-radius: 3px;
font-size: 14px;
}
.form-group textarea {
resize: vertical;
min-height: 300px;
}
.form-actions {
display: flex;
gap: 0.5rem;
justify-content: flex-end;
margin-top: 1rem;
.form-group input[type="checkbox"] {
margin-right: 5px;
}
</style>
{% endblock %}
<!--
Copyright (C) 2026 Stefy Lanza <stefy@nexlab.net>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
{% extends "base.html" %}
{% block title %}My Rotations - AISBF{% endblock %}
{% block content %}
<div class="container">
<h1>My Rotations</h1>
<p>Manage your personal rotation configurations</p>
{% if success %}
<div class="alert alert-success">{{ success }}</div>
{% endif %}
{% if error %}
<div class="alert alert-error">{{ error }}</div>
{% endif %}
<div class="card">
<h2>Rotation Configurations</h2>
<div id="rotations-list">
<!-- Rotations will be loaded here -->
<h2 style="margin-bottom: 30px;">My Rotations Configuration</h2>
{% if success %}
<div class="alert alert-success">{{ success }}</div>
{% endif %}
{% if error %}
<div class="alert alert-error">{{ error }}</div>
{% endif %}
<div id="rotations-list" style="margin-bottom: 20px;">
<!-- Rotations list will be rendered here -->
</div>
<button type="button" class="btn" onclick="addRotation()" style="margin-top: 20px;">Add Rotation</button>
<div style="display: flex; gap: 10px; margin-top: 20px;">
<button type="button" class="btn" onclick="saveRotations()">Save Configuration</button>
<a href="{{ url_for(request, '/dashboard') }}" class="btn btn-secondary">Cancel</a>
</div>
<script>
const rotationsData = {
config: {{ rotations_json | safe }},
providers: {{ available_providers | safe }},
saveUrl: "{{ url_for(request, '/dashboard/rotations') }}",
successUrl: "{{ url_for(request, '/dashboard/rotations?success=1') }}"
};
let rotationsConfig = rotationsData.config;
let availableProviders = rotationsData.providers;
let expandedRotations = new Set();
function renderRotationsList() {
const container = document.getElementById('rotations-list');
container.innerHTML = '';
Object.entries(rotationsConfig.rotations || {}).forEach(([key, rotation]) => {
const rotationItem = document.createElement('div');
rotationItem.className = 'rotation-item';
rotationItem.style.cssText = 'border: 1px solid #0f3460; margin-bottom: 10px; border-radius: 5px; background: #1a1a2e;';
const isExpanded = expandedRotations.has(key);
const providerCount = rotation.providers ? rotation.providers.length : 0;
rotationItem.innerHTML = `
<div class="rotation-header" onclick="toggleRotation('${key}')" style="padding: 15px; cursor: pointer; display: flex; justify-content: space-between; align-items: center; user-select: none;">
<div style="display: flex; align-items: center; gap: 10px;">
<span style="font-size: 18px;">${isExpanded ? '▼' : '▶'}</span>
<strong style="font-size: 16px;">${key}</strong>
<span style="color: #a0a0a0; font-size: 14px;">(${providerCount} provider${providerCount !== 1 ? 's' : ''})</span>
</div>
<button class="btn btn-primary" onclick="showAddRotationModal()">Add New Rotation</button>
<button class="btn btn-success" onclick="applyChanges()" style="margin-left: 10px;">✓ Apply Changes</button>
<button type="button" class="btn btn-secondary" onclick="event.stopPropagation(); removeRotation('${key}')" style="background: #dc3545; padding: 5px 15px;">Remove</button>
</div>
</div>
<div id="rotation-details-${key}" style="display: ${isExpanded ? 'block' : 'none'}; padding: 20px; border-top: 1px solid #0f3460; background: #16213e;">
<!-- Details will be rendered here -->
</div>
`;
container.appendChild(rotationItem);
<!-- Modal for adding/editing rotations -->
<div id="rotation-modal" class="modal" style="display: none;">
<div class="modal-content">
<div class="modal-header">
<h3 id="modal-title">Add Rotation</h3>
<button class="close-btn" onclick="closeModal()">&times;</button>
if (isExpanded) {
renderRotationDetails(key);
}
});
}
function toggleRotation(key) {
if (expandedRotations.has(key)) {
expandedRotations.delete(key);
} else {
expandedRotations.add(key);
}
renderRotationsList();
}
function renderRotationDetails(rotationKey) {
const container = document.getElementById(`rotation-details-${rotationKey}`);
const rotation = rotationsConfig.rotations[rotationKey];
container.innerHTML = `
<div class="form-group">
<label>Model Name</label>
<input type="text" value="${rotation.model_name}" onchange="updateRotation('${rotationKey}', 'model_name', this.value)" required>
</div>
<div class="modal-body">
<form id="rotation-form">
<div class="form-group">
<label for="rotation-name">Rotation Name:</label>
<input type="text" id="rotation-name" required>
<label>
<input type="checkbox" ${rotation.notifyerrors ? 'checked' : ''} onchange="updateRotation('${rotationKey}', 'notifyerrors', this.checked)">
Notify Errors
</label>
</div>
<div class="form-group">
<label for="rotation-config">Rotation Configuration (JSON):</label>
<textarea id="rotation-config" rows="20" placeholder='{"model_name": "rotation-name", "providers": []}'></textarea>
<label>Capabilities (comma-separated)</label>
<input type="text" value="${rotation.capabilities ? rotation.capabilities.join(', ') : ''}" onchange="updateRotationCapabilities('${rotationKey}', this.value)" placeholder="e.g., code_generation, t2t, reasoning">
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Save</button>
<button type="button" class="btn btn-secondary" onclick="closeModal()">Cancel</button>
<div class="form-group">
<label>Default Rate Limit (seconds)</label>
<input type="number" value="${rotation.default_rate_limit || ''}" onchange="updateRotation('${rotationKey}', 'default_rate_limit', this.value ? parseFloat(this.value) : null)" step="0.1" placeholder="Optional">
</div>
</form>
<div class="form-group">
<label>Default Context Size</label>
<input type="number" value="${rotation.default_context_size || ''}" onchange="updateRotation('${rotationKey}', 'default_context_size', this.value ? parseInt(this.value) : null)" placeholder="Optional">
</div>
<div class="form-group">
<label>
<input type="checkbox" ${rotation.nsfw ? 'checked' : ''} onchange="updateRotation('${rotationKey}', 'nsfw', this.checked)">
NSFW
</label>
</div>
</div>
<script>
let rotations = {{ user_rotations_json | safe }};
let currentEditingIndex = -1;
<div class="form-group">
<label>
<input type="checkbox" ${rotation.privacy ? 'checked' : ''} onchange="updateRotation('${rotationKey}', 'privacy', this.checked)">
Privacy
</label>
</div>
function renderRotations() {
const container = document.getElementById('rotations-list');
container.innerHTML = '';
<h4 style="margin-top: 20px; margin-bottom: 10px;">Providers</h4>
<div id="providers-${rotationKey}"></div>
<button type="button" class="btn btn-secondary" onclick="addRotationProvider('${rotationKey}')" style="margin-top: 10px;">Add Provider</button>
`;
if (rotations.length === 0) {
container.innerHTML = '<p class="empty-state">No rotations configured yet. Click "Add New Rotation" to get started.</p>';
renderRotationProviders(rotationKey);
}
function renderRotationProviders(rotationKey) {
const container = document.getElementById(`providers-${rotationKey}`);
const rotation = rotationsConfig.rotations[rotationKey];
if (!rotation.providers || rotation.providers.length === 0) {
container.innerHTML = '<p style="color: #a0a0a0;">No providers configured</p>';
return;
}
rotations.forEach((rotation, index) => {
const div = document.createElement('div');
div.className = 'rotation-item';
div.innerHTML = `
<div class="rotation-header">
<h3>${rotation.rotation_id}</h3>
<div class="rotation-actions">
<button class="btn btn-secondary btn-sm" onclick="editRotation(${index})">Edit</button>
<button class="btn btn-danger btn-sm" onclick="deleteRotation('${rotation.rotation_id}')">Delete</button>
container.innerHTML = '';
rotation.providers.forEach((provider, providerIndex) => {
const providerDiv = document.createElement('div');
providerDiv.style.cssText = 'border: 1px solid #0f3460; padding: 15px; margin-bottom: 10px; border-radius: 3px; background: #1a1a2e;';
const providerOptions = availableProviders.map(p =>
`<option value="${p}" ${provider.provider_id === p ? 'selected' : ''}>${p}</option>`
).join('');
providerDiv.innerHTML = `
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
<strong>Provider ${providerIndex + 1}</strong>
<button type="button" class="btn btn-secondary" onclick="removeRotationProvider('${rotationKey}', ${providerIndex})" style="background: #dc3545; padding: 5px 10px; font-size: 12px;">Remove</button>
</div>
<div class="form-group">
<label>Provider ID</label>
<select onchange="updateRotationProvider('${rotationKey}', ${providerIndex}, 'provider_id', this.value)" required>
<option value="">Select provider...</option>
${providerOptions}
</select>
</div>
<div class="rotation-details">
<p><strong>Created:</strong> ${new Date(rotation.created_at).toLocaleString()}</p>
<p><strong>Last Updated:</strong> ${new Date(rotation.updated_at).toLocaleString()}</p>
<details>
<summary>Configuration (JSON)</summary>
<pre>${JSON.stringify(rotation.config, null, 2)}</pre>
</details>
<div class="form-group">
<label>Weight (optional, for provider-level weight)</label>
<input type="number" value="${provider.weight || ''}" onchange="updateRotationProvider('${rotationKey}', ${providerIndex}, 'weight', this.value ? parseInt(this.value) : null)" placeholder="Optional">
</div>
<h5 style="margin-top: 15px; margin-bottom: 10px;">Models</h5>
<div id="models-${rotationKey}-${providerIndex}"></div>
<button type="button" class="btn btn-secondary" onclick="addRotationModel('${rotationKey}', ${providerIndex})" style="margin-top: 10px; font-size: 12px;">Add Model</button>
<p style="font-size: 12px; color: #a0a0a0; margin-top: 5px;">Leave models empty to use all models from provider config</p>
`;
container.appendChild(div);
container.appendChild(providerDiv);
renderRotationModels(rotationKey, providerIndex);
});
}
function showAddRotationModal() {
currentEditingIndex = -1;
document.getElementById('modal-title').textContent = 'Add Rotation';
document.getElementById('rotation-name').value = '';
document.getElementById('rotation-config').value = '{"model_name": "my-rotation", "providers": []}';
document.getElementById('rotation-modal').style.display = 'block';
}
function renderRotationModels(rotationKey, providerIndex) {
const container = document.getElementById(`models-${rotationKey}-${providerIndex}`);
const provider = rotationsConfig.rotations[rotationKey].providers[providerIndex];
function editRotation(index) {
currentEditingIndex = index;
const rotation = rotations[index];
document.getElementById('modal-title').textContent = 'Edit Rotation';
document.getElementById('rotation-name').value = rotation.rotation_id;
document.getElementById('rotation-name').readOnly = true;
document.getElementById('rotation-config').value = JSON.stringify(rotation.config, null, 2);
document.getElementById('rotation-modal').style.display = 'block';
}
if (!provider.models || provider.models.length === 0) {
container.innerHTML = '<p style="color: #a0a0a0; font-size: 12px;">No models specified (will use all from provider)</p>';
return;
}
function closeModal() {
document.getElementById('rotation-modal').style.display = 'none';
document.getElementById('rotation-name').readOnly = false;
currentEditingIndex = -1;
}
container.innerHTML = '';
provider.models.forEach((model, modelIndex) => {
const modelDiv = document.createElement('div');
modelDiv.style.cssText = 'border: 1px solid #0f3460; padding: 10px; margin-bottom: 8px; border-radius: 3px; background: #16213e;';
modelDiv.innerHTML = `
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
<strong style="font-size: 13px;">Model ${modelIndex + 1}</strong>
<button type="button" onclick="removeRotationModel('${rotationKey}', ${providerIndex}, ${modelIndex})" style="background: #dc3545; color: white; border: none; padding: 3px 8px; border-radius: 3px; cursor: pointer; font-size: 11px;">Remove</button>
</div>
function deleteRotation(rotationName) {
if (!confirm(`Are you sure you want to delete the rotation "${rotationName}"? This action cannot be undone.`)) return;
<div class="form-group" style="margin-bottom: 8px;">
<label style="font-size: 12px;">Model Name</label>
<input type="text" value="${model.name}" onchange="updateRotationModel('${rotationKey}', ${providerIndex}, ${modelIndex}, 'name', this.value)" required style="font-size: 12px; padding: 5px;">
</div>
fetch('{{ url_for(request, "/dashboard/user/rotations") }}/' + encodeURIComponent(rotationName), {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
}
}).then(response => {
if (response.ok) {
location.reload();
} else {
return response.json().then(data => {
throw new Error(data.error || 'Failed to delete rotation');
});
}
}).catch(error => {
alert('Error: ' + error.message);
});
}
<div class="form-group" style="margin-bottom: 8px;">
<label style="font-size: 12px;">Weight</label>
<input type="number" value="${model.weight || 1}" onchange="updateRotationModel('${rotationKey}', ${providerIndex}, ${modelIndex}, 'weight', parseInt(this.value))" style="font-size: 12px; padding: 5px;">
</div>
// Handle form submission
document.getElementById('rotation-form').addEventListener('submit', function(e) {
e.preventDefault();
<div class="form-group" style="margin-bottom: 8px;">
<label style="font-size: 12px;">Rate Limit (seconds)</label>
<input type="number" value="${model.rate_limit || 0}" onchange="updateRotationModel('${rotationKey}', ${providerIndex}, ${modelIndex}, 'rate_limit', parseFloat(this.value))" step="0.1" style="font-size: 12px; padding: 5px;">
</div>
const rotationName = document.getElementById('rotation-name').value.trim();
const configText = document.getElementById('rotation-config').value.trim();
<div class="form-group" style="margin-bottom: 8px;">
<label style="font-size: 12px;">Max Request Tokens</label>
<input type="number" value="${model.max_request_tokens || ''}" onchange="updateRotationModel('${rotationKey}', ${providerIndex}, ${modelIndex}, 'max_request_tokens', this.value ? parseInt(this.value) : null)" placeholder="Optional" style="font-size: 12px; padding: 5px;">
</div>
if (!rotationName) {
alert('Rotation name is required');
return;
}
<div class="form-group" style="margin-bottom: 8px;">
<label style="font-size: 12px;">Context Size</label>
<input type="number" value="${model.context_size || ''}" onchange="updateRotationModel('${rotationKey}', ${providerIndex}, ${modelIndex}, 'context_size', this.value ? parseInt(this.value) : null)" placeholder="Optional" style="font-size: 12px; padding: 5px;">
</div>
if (!configText) {
alert('Rotation configuration is required');
return;
}
<div class="form-group" style="margin-bottom: 8px;">
<label style="font-size: 12px;">Condense Context (%)</label>
<input type="number" value="${model.condense_context || ''}" onchange="updateRotationModel('${rotationKey}', ${providerIndex}, ${modelIndex}, 'condense_context', this.value ? parseInt(this.value) : null)" placeholder="Optional (default: 80)" style="font-size: 12px; padding: 5px;">
</div>
let configObj;
try {
configObj = JSON.parse(configText);
} catch (e) {
alert('Invalid JSON configuration: ' + e.message);
return;
}
<div class="form-group" style="margin-bottom: 0;">
<label style="font-size: 12px;">Condense Method</label>
<input type="text" value="${Array.isArray(model.condense_method) ? model.condense_method.join(', ') : (model.condense_method || '')}" onchange="updateRotationModelCondenseMethod('${rotationKey}', ${providerIndex}, ${modelIndex}, this.value)" placeholder="e.g., semantic, conversational, hierarchical" style="font-size: 12px; padding: 5px;">
</div>
`;
const formData = new FormData();
formData.append('rotation_name', rotationName);
formData.append('rotation_config', JSON.stringify(configObj));
container.appendChild(modelDiv);
});
}
const url = '{{ url_for(request, "/dashboard/user/rotations") }}';
const method = currentEditingIndex >= 0 ? 'PUT' : 'POST';
function addRotation() {
const key = prompt('Enter rotation key (e.g., "coding", "general"):');
if (!key || rotationsConfig.rotations[key]) {
alert('Invalid or duplicate rotation key');
return;
}
fetch(url, {
method: method,
body: formData
}).then(response => {
if (response.ok) {
closeModal();
location.reload();
} else {
return response.text().then(text => {
throw new Error(text || 'Failed to save rotation');
});
if (!rotationsConfig.rotations) {
rotationsConfig.rotations = {};
}
}).catch(error => {
alert('Error: ' + error.message);
});
});
async function applyChanges() {
const button = event.target;
const originalText = button.innerHTML;
rotationsConfig.rotations[key] = {
model_name: key,
notifyerrors: false,
capabilities: [],
providers: []
};
try {
button.innerHTML = '🔄 Reloading...';
button.disabled = true;
expandedRotations.add(key);
renderRotationsList();
}
const response = await fetch('{{ url_for(request, "/dashboard/user/reload-config") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
function removeRotation(key) {
if (confirm(`Remove rotation "${key}"?`)) {
delete rotationsConfig.rotations[key];
expandedRotations.delete(key);
renderRotationsList();
}
});
}
if (response.ok) {
button.innerHTML = '✓ Applied Successfully!';
button.style.background = '#10b981';
setTimeout(() => {
button.innerHTML = originalText;
button.style.background = '';
button.disabled = false;
}, 2000);
} else {
const data = await response.json();
throw new Error(data.error || 'Failed to apply changes');
function updateRotation(key, field, value) {
rotationsConfig.rotations[key][field] = value;
}
function updateRotationCapabilities(rotationKey, value) {
const trimmed = value.trim();
if (!trimmed) {
rotationsConfig.rotations[rotationKey].capabilities = null;
return;
}
} catch (error) {
button.innerHTML = '✗ Error';
button.style.background = '#ef4444';
setTimeout(() => {
button.innerHTML = originalText;
button.style.background = '';
button.disabled = false;
}, 2000);
// Split by comma and clean up
rotationsConfig.rotations[rotationKey].capabilities =
trimmed.split(',').map(s => s.trim()).filter(s => s);
}
alert('Error applying changes: ' + error.message);
function addRotationProvider(rotationKey) {
if (!rotationsConfig.rotations[rotationKey].providers) {
rotationsConfig.rotations[rotationKey].providers = [];
}
}
renderRotations();
</script>
rotationsConfig.rotations[rotationKey].providers.push({
provider_id: '',
models: []
});
<style>
.rotation-item {
background: #1a1a2e;
padding: 1rem;
margin: 1rem 0;
border-radius: 8px;
renderRotationProviders(rotationKey);
}
.rotation-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
function removeRotationProvider(rotationKey, providerIndex) {
if (confirm('Remove this provider?')) {
rotationsConfig.rotations[rotationKey].providers.splice(providerIndex, 1);
renderRotationProviders(rotationKey);
}
}
.rotation-header h3 {
margin: 0;
function updateRotationProvider(rotationKey, providerIndex, field, value) {
rotationsConfig.rotations[rotationKey].providers[providerIndex][field] = value;
}
.rotation-actions {
display: flex;
gap: 0.5rem;
}
function addRotationModel(rotationKey, providerIndex) {
const provider = rotationsConfig.rotations[rotationKey].providers[providerIndex];
if (!provider.models) {
provider.models = [];
}
.rotation-details p {
margin: 0.5rem 0;
}
provider.models.push({
name: '',
weight: 1,
rate_limit: 0
});
.rotation-details pre {
background: #0f3460;
padding: 1rem;
border-radius: 4px;
overflow-x: auto;
white-space: pre-wrap;
word-wrap: break-word;
renderRotationModels(rotationKey, providerIndex);
}
.empty-state {
text-align: center;
color: #a0a0a0;
padding: 2rem;
function removeRotationModel(rotationKey, providerIndex, modelIndex) {
if (confirm('Remove this model?')) {
rotationsConfig.rotations[rotationKey].providers[providerIndex].models.splice(modelIndex, 1);
renderRotationModels(rotationKey, providerIndex);
}
}
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
function updateRotationModel(rotationKey, providerIndex, modelIndex, field, value) {
rotationsConfig.rotations[rotationKey].providers[providerIndex].models[modelIndex][field] = value;
}
.modal-content {
background: #1a1a2e;
border-radius: 8px;
width: 90%;
max-width: 600px;
max-height: 80vh;
overflow-y: auto;
function updateRotationModelCondenseMethod(rotationKey, providerIndex, modelIndex, value) {
const trimmed = value.trim();
if (!trimmed) {
rotationsConfig.rotations[rotationKey].providers[providerIndex].models[modelIndex].condense_method = null;
return;
}
// Check if it's a comma-separated list
if (trimmed.includes(',')) {
rotationsConfig.rotations[rotationKey].providers[providerIndex].models[modelIndex].condense_method =
trimmed.split(',').map(s => s.trim()).filter(s => s);
} else {
rotationsConfig.rotations[rotationKey].providers[providerIndex].models[modelIndex].condense_method = trimmed;
}
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
border-bottom: 1px solid #0f3460;
async function saveRotations() {
try {
const response = await fetch(rotationsData.saveUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'config=' + encodeURIComponent(JSON.stringify(rotationsConfig, null, 2))
});
if (response.ok) {
window.location.href = rotationsData.successUrl;
} else {
alert('Error saving configuration');
}
} catch (error) {
alert('Error: ' + error.message);
}
}
.modal-header h3 {
margin: 0;
// Initial render
renderRotationsList();
</script>
<style>
.rotation-item {
animation: fadeIn 0.3s;
}
.close-btn {
background: none;
border: none;
color: #e0e0e0;
font-size: 1.5rem;
cursor: pointer;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
.rotation-header:hover {
background: #0f3460;
}
.modal-body {
padding: 1rem;
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
.form-group {
margin-bottom: 1rem;
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
margin-bottom: 5px;
font-weight: 500;
color: #e0e0e0;
}
.form-group input[type="text"],
.form-group textarea {
.form-group input[type="number"],
.form-group select {
width: 100%;
padding: 0.5rem;
padding: 8px;
border: 1px solid #0f3460;
border-radius: 4px;
background: #16213e;
color: #e0e0e0;
font-family: monospace;
border-radius: 3px;
font-size: 14px;
background: #1a1a2e;
color: #e0e0e0;
}
.form-group textarea {
resize: vertical;
min-height: 300px;
}
.form-actions {
display: flex;
gap: 0.5rem;
justify-content: flex-end;
margin-top: 1rem;
.form-group input[type="checkbox"] {
margin-right: 5px;
}
</style>
{% endblock %}
#!/bin/bash
# Release Verification Script for AISBF v0.99.26
# Release Verification Script for AISBF v0.99.37
echo "================================================================================"
echo " AISBF v0.99.26 Release Verification"
echo " AISBF v0.99.37 Release Verification"
echo "================================================================================"
echo
......@@ -26,7 +26,7 @@ check() {
# 1. Check version numbers
echo "1. Checking version numbers..."
VERSION="0.99.26"
VERSION="0.99.37"
grep -q "version=\"$VERSION\"" setup.py
check "setup.py version is $VERSION"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment