mirror of
https://github.com/perstarkse/minne.git
synced 2026-01-11 20:50:24 +01:00
design: improved admin page, new structure
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
- Improved admin page, now only loads models when specifically requested
|
||||||
|
|
||||||
## Version 0.2.6 (2025-10-29)
|
## Version 0.2.6 (2025-10-29)
|
||||||
- Added an opt-in FastEmbed-based reranking stage behind `reranking_enabled`. It improves retrieval accuracy by re-scoring hybrid results.
|
- Added an opt-in FastEmbed-based reranking stage behind `reranking_enabled`. It improves retrieval accuracy by re-scoring hybrid results.
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
use async_openai::types::ListModelResponse;
|
use async_openai::types::ListModelResponse;
|
||||||
use axum::{extract::State, response::IntoResponse, Form};
|
use axum::{
|
||||||
|
extract::{Query, State},
|
||||||
|
response::IntoResponse,
|
||||||
|
Form,
|
||||||
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use common::{
|
use common::{
|
||||||
@@ -31,44 +35,83 @@ use crate::{
|
|||||||
pub struct AdminPanelData {
|
pub struct AdminPanelData {
|
||||||
user: User,
|
user: User,
|
||||||
settings: SystemSettings,
|
settings: SystemSettings,
|
||||||
analytics: Analytics,
|
analytics: Option<Analytics>,
|
||||||
users: i64,
|
users: Option<i64>,
|
||||||
default_query_prompt: String,
|
default_query_prompt: String,
|
||||||
default_image_prompt: String,
|
default_image_prompt: String,
|
||||||
conversation_archive: Vec<Conversation>,
|
conversation_archive: Vec<Conversation>,
|
||||||
available_models: ListModelResponse,
|
available_models: Option<ListModelResponse>,
|
||||||
|
current_section: AdminSection,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum AdminSection {
|
||||||
|
Overview,
|
||||||
|
Models,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for AdminSection {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Overview
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct AdminPanelQuery {
|
||||||
|
section: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn show_admin_panel(
|
pub async fn show_admin_panel(
|
||||||
State(state): State<HtmlState>,
|
State(state): State<HtmlState>,
|
||||||
RequireUser(user): RequireUser,
|
RequireUser(user): RequireUser,
|
||||||
|
Query(query): Query<AdminPanelQuery>,
|
||||||
) -> Result<impl IntoResponse, HtmlError> {
|
) -> Result<impl IntoResponse, HtmlError> {
|
||||||
let (
|
let section = match query.section.as_deref() {
|
||||||
settings_res,
|
Some("models") => AdminSection::Models,
|
||||||
analytics_res,
|
_ => AdminSection::Overview,
|
||||||
user_count_res,
|
};
|
||||||
conversation_archive_res,
|
|
||||||
available_models_res,
|
let (settings, conversation_archive) = tokio::try_join!(
|
||||||
) = tokio::join!(
|
|
||||||
SystemSettings::get_current(&state.db),
|
SystemSettings::get_current(&state.db),
|
||||||
Analytics::get_current(&state.db),
|
User::get_user_conversations(&user.id, &state.db)
|
||||||
Analytics::get_users_amount(&state.db),
|
)?;
|
||||||
User::get_user_conversations(&user.id, &state.db),
|
|
||||||
async { state.openai_client.models().list().await }
|
let (analytics, users) = if section == AdminSection::Overview {
|
||||||
);
|
let (analytics, users) = tokio::try_join!(
|
||||||
|
Analytics::get_current(&state.db),
|
||||||
|
Analytics::get_users_amount(&state.db)
|
||||||
|
)?;
|
||||||
|
(Some(analytics), Some(users))
|
||||||
|
} else {
|
||||||
|
(None, None)
|
||||||
|
};
|
||||||
|
|
||||||
|
let available_models = if section == AdminSection::Models {
|
||||||
|
Some(
|
||||||
|
state
|
||||||
|
.openai_client
|
||||||
|
.models()
|
||||||
|
.list()
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppError::InternalError(e.to_string()))?,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
Ok(TemplateResponse::new_template(
|
Ok(TemplateResponse::new_template(
|
||||||
"admin/base.html",
|
"admin/base.html",
|
||||||
AdminPanelData {
|
AdminPanelData {
|
||||||
user,
|
user,
|
||||||
settings: settings_res?,
|
settings,
|
||||||
analytics: analytics_res?,
|
analytics,
|
||||||
available_models: available_models_res
|
available_models,
|
||||||
.map_err(|e| AppError::InternalError(e.to_string()))?,
|
users,
|
||||||
users: user_count_res?,
|
|
||||||
default_query_prompt: DEFAULT_QUERY_SYSTEM_PROMPT.to_string(),
|
default_query_prompt: DEFAULT_QUERY_SYSTEM_PROMPT.to_string(),
|
||||||
default_image_prompt: DEFAULT_IMAGE_PROCESSING_PROMPT.to_string(),
|
default_image_prompt: DEFAULT_IMAGE_PROCESSING_PROMPT.to_string(),
|
||||||
conversation_archive: conversation_archive_res?,
|
conversation_archive,
|
||||||
|
current_section: section,
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
@@ -115,7 +158,7 @@ pub async fn toggle_registration_status(
|
|||||||
SystemSettings::update(&state.db, new_settings.clone()).await?;
|
SystemSettings::update(&state.db, new_settings.clone()).await?;
|
||||||
|
|
||||||
Ok(TemplateResponse::new_partial(
|
Ok(TemplateResponse::new_partial(
|
||||||
"admin/base.html",
|
"admin/sections/overview.html",
|
||||||
"registration_status_input",
|
"registration_status_input",
|
||||||
RegistrationToggleData {
|
RegistrationToggleData {
|
||||||
settings: new_settings,
|
settings: new_settings,
|
||||||
@@ -217,7 +260,7 @@ pub async fn update_model_settings(
|
|||||||
.map_err(|_e| AppError::InternalError("Failed to get models".to_string()))?;
|
.map_err(|_e| AppError::InternalError("Failed to get models".to_string()))?;
|
||||||
|
|
||||||
Ok(TemplateResponse::new_partial(
|
Ok(TemplateResponse::new_partial(
|
||||||
"admin/base.html",
|
"admin/sections/models.html",
|
||||||
"model_settings_form",
|
"model_settings_form",
|
||||||
ModelSettingsData {
|
ModelSettingsData {
|
||||||
settings: new_settings,
|
settings: new_settings,
|
||||||
@@ -282,7 +325,7 @@ pub async fn patch_query_prompt(
|
|||||||
SystemSettings::update(&state.db, new_settings.clone()).await?;
|
SystemSettings::update(&state.db, new_settings.clone()).await?;
|
||||||
|
|
||||||
Ok(TemplateResponse::new_partial(
|
Ok(TemplateResponse::new_partial(
|
||||||
"admin/base.html",
|
"admin/sections/overview.html",
|
||||||
"system_prompt_section",
|
"system_prompt_section",
|
||||||
SystemPromptSectionData {
|
SystemPromptSectionData {
|
||||||
settings: new_settings,
|
settings: new_settings,
|
||||||
@@ -341,7 +384,7 @@ pub async fn patch_ingestion_prompt(
|
|||||||
SystemSettings::update(&state.db, new_settings.clone()).await?;
|
SystemSettings::update(&state.db, new_settings.clone()).await?;
|
||||||
|
|
||||||
Ok(TemplateResponse::new_partial(
|
Ok(TemplateResponse::new_partial(
|
||||||
"admin/base.html",
|
"admin/sections/overview.html",
|
||||||
"system_prompt_section",
|
"system_prompt_section",
|
||||||
SystemPromptSectionData {
|
SystemPromptSectionData {
|
||||||
settings: new_settings,
|
settings: new_settings,
|
||||||
@@ -400,7 +443,7 @@ pub async fn patch_image_prompt(
|
|||||||
SystemSettings::update(&state.db, new_settings.clone()).await?;
|
SystemSettings::update(&state.db, new_settings.clone()).await?;
|
||||||
|
|
||||||
Ok(TemplateResponse::new_partial(
|
Ok(TemplateResponse::new_partial(
|
||||||
"admin/base.html",
|
"admin/sections/overview.html",
|
||||||
"system_prompt_section",
|
"system_prompt_section",
|
||||||
SystemPromptSectionData {
|
SystemPromptSectionData {
|
||||||
settings: new_settings,
|
settings: new_settings,
|
||||||
|
|||||||
@@ -3,154 +3,49 @@
|
|||||||
{% block title %}Minne - Admin{% endblock %}
|
{% block title %}Minne - Admin{% endblock %}
|
||||||
|
|
||||||
{% block main %}
|
{% block main %}
|
||||||
<div class="flex justify-center grow mt-2 sm:mt-4 pb-4">
|
<div id="admin-shell" class="flex justify-center grow mt-2 sm:mt-4 pb-4">
|
||||||
<div class="container">
|
<div class="container flex flex-col gap-4">
|
||||||
<section class="mb-4">
|
<section class="nb-panel p-4 sm:p-5 flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||||
<div class="nb-panel p-3 flex items-center justify-between">
|
<div>
|
||||||
<h1 class="text-xl font-extrabold tracking-tight">Admin Dashboard</h1>
|
<h1 class="text-xl font-extrabold tracking-tight">Admin Controls</h1>
|
||||||
|
<p class="text-sm opacity-70 max-w-2xl">
|
||||||
|
Stay on top of analytics and manage AI integrations without waiting on long-running model calls.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="text-xs opacity-60 sm:text-right">
|
||||||
|
Signed in as <span class="font-medium">{{ user.email }}</span>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="mb-4">
|
<nav
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
class="nb-panel p-2 flex flex-wrap gap-2 text-sm"
|
||||||
<div class="nb-stat">
|
hx-boost="true"
|
||||||
<div class="text-xs opacity-70">Page Loads</div>
|
hx-target="#admin-shell"
|
||||||
<div class="text-3xl font-extrabold">{{analytics.page_loads}}</div>
|
hx-select="#admin-shell"
|
||||||
<div class="text-xs opacity-60">Total page load events</div>
|
hx-swap="outerHTML"
|
||||||
</div>
|
hx-push-url="true"
|
||||||
<div class="nb-stat">
|
>
|
||||||
<div class="text-xs opacity-70">Unique Visitors</div>
|
<a
|
||||||
<div class="text-3xl font-extrabold">{{analytics.visitors}}</div>
|
href="/admin?section=overview"
|
||||||
<div class="text-xs opacity-60">Distinct users by fingerprint</div>
|
class="nb-btn btn-sm px-4 {% if current_section == 'overview' %}nb-cta{% else %}btn-ghost{% endif %}"
|
||||||
</div>
|
>
|
||||||
<div class="nb-stat">
|
Overview
|
||||||
<div class="text-xs opacity-70">Users</div>
|
</a>
|
||||||
<div class="text-3xl font-extrabold">{{users}}</div>
|
<a
|
||||||
<div class="text-xs opacity-60">Registered accounts</div>
|
href="/admin?section=models"
|
||||||
</div>
|
class="nb-btn btn-sm px-4 {% if current_section == 'models' %}nb-cta{% else %}btn-ghost{% endif %}"
|
||||||
</div>
|
>
|
||||||
</section>
|
Models
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
<section class="grid grid-cols-1 xl:grid-cols-2 gap-4">
|
<div id="admin-content" class="flex flex-col gap-4">
|
||||||
{% block system_prompt_section %}
|
{% if current_section == 'models' %}
|
||||||
<div id="system_prompt_section" class="nb-panel p-4">
|
{% include 'admin/sections/models.html' %}
|
||||||
<div class="text-sm font-semibold mb-3">System Prompts</div>
|
{% else %}
|
||||||
<div class="flex gap-2 flex-col sm:flex-row">
|
{% include 'admin/sections/overview.html' %}
|
||||||
<button type="button" class="nb-btn btn-sm" hx-get="/edit-query-prompt" hx-target="#modal" hx-swap="innerHTML">Edit Query Prompt</button>
|
{% endif %}
|
||||||
<button type="button" class="nb-btn btn-sm" hx-get="/edit-ingestion-prompt" hx-target="#modal" hx-swap="innerHTML">Edit Ingestion Prompt</button>
|
</div>
|
||||||
<button type="button" class="nb-btn btn-sm" hx-get="/edit-image-prompt" hx-target="#modal" hx-swap="innerHTML">Edit Image Prompt</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
<div class="nb-panel p-4">
|
|
||||||
<div class="text-sm font-semibold mb-3">AI Models</div>
|
|
||||||
{% block model_settings_form %}
|
|
||||||
<form hx-patch="/update-model-settings" hx-swap="outerHTML" class="grid grid-cols-1 gap-4">
|
|
||||||
<!-- Query Model -->
|
|
||||||
<div>
|
|
||||||
<div class="text-sm opacity-80 mb-1">Query Model</div>
|
|
||||||
<select name="query_model" class="nb-select w-full">
|
|
||||||
{% for model in available_models.data %}
|
|
||||||
<option value="{{model.id}}" {% if settings.query_model==model.id %} selected {% endif %}>{{model.id}}</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
<p class="text-xs opacity-70 mt-1">Current: <span class="font-mono">{{settings.query_model}}</span></p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Processing Model -->
|
|
||||||
<div>
|
|
||||||
<div class="text-sm opacity-80 mb-1">Processing Model</div>
|
|
||||||
<select name="processing_model" class="nb-select w-full">
|
|
||||||
{% for model in available_models.data %}
|
|
||||||
<option value="{{model.id}}" {% if settings.processing_model==model.id %} selected {% endif %}>{{model.id}}</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
<p class="text-xs opacity-70 mt-1">Current: <span class="font-mono">{{settings.processing_model}}</span></p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Image Processing Model -->
|
|
||||||
<div>
|
|
||||||
<div class="text-sm opacity-80 mb-1">Image Processing Model</div>
|
|
||||||
<select name="image_processing_model" class="nb-select w-full">
|
|
||||||
{% for model in available_models.data %}
|
|
||||||
<option value="{{model.id}}" {% if settings.image_processing_model==model.id %} selected {% endif %}>{{model.id}}</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
<p class="text-xs opacity-70 mt-1">Current: <span class="font-mono">{{settings.image_processing_model}}</span></p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Voice Processing Model -->
|
|
||||||
<div>
|
|
||||||
<div class="text-sm opacity-80 mb-1">Voice Processing Model</div>
|
|
||||||
<select name="voice_processing_model" class="nb-select w-full">
|
|
||||||
{% for model in available_models.data %}
|
|
||||||
<option value="{{model.id}}" {% if settings.voice_processing_model==model.id %} selected {% endif %}>{{model.id}}</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
<p class="text-xs opacity-70 mt-1">Current: <span class="font-mono">{{settings.voice_processing_model}}</span></p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Embedding Model -->
|
|
||||||
<div>
|
|
||||||
<div class="text-sm opacity-80 mb-1">Embedding Model</div>
|
|
||||||
<select name="embedding_model" class="nb-select w-full">
|
|
||||||
{% for model in available_models.data %}
|
|
||||||
<option value="{{model.id}}" {% if settings.embedding_model==model.id %} selected {% endif %}>{{model.id}}</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
<p class="text-xs opacity-70 mt-1">Current: <span class="font-mono">{{settings.embedding_model}} ({{settings.embedding_dimensions}} dims)</span></p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Embedding Dimensions -->
|
|
||||||
<div>
|
|
||||||
<div class="text-sm opacity-80 mb-1" for="embedding_dimensions">Embedding Dimensions</div>
|
|
||||||
<input type="number" id="embedding_dimensions" name="embedding_dimensions" class="nb-input w-full" value="{{ settings.embedding_dimensions }}" required />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Alert -->
|
|
||||||
<div id="embedding-change-alert" class="nb-panel p-3 bg-warning/20 hidden">
|
|
||||||
<div class="text-sm"><strong>Warning:</strong> Changing dimensions will require re-creating all embeddings. Look up your model's required dimensions or use a model that allows specifying them.</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex justify-end">
|
|
||||||
<button type="submit" class="nb-btn nb-cta btn-sm">Save Model Settings</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// Rebind after HTMX swaps
|
|
||||||
(() => {
|
|
||||||
const dimensionInput = document.getElementById('embedding_dimensions');
|
|
||||||
const alertElement = document.getElementById('embedding-change-alert');
|
|
||||||
const initialDimensions = '{{ settings.embedding_dimensions }}';
|
|
||||||
if (dimensionInput && alertElement) {
|
|
||||||
dimensionInput.addEventListener('input', (event) => {
|
|
||||||
if (String(event.target.value) !== String(initialDimensions)) {
|
|
||||||
alertElement.classList.remove('hidden');
|
|
||||||
} else {
|
|
||||||
alertElement.classList.add('hidden');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="nb-panel p-4">
|
|
||||||
<div class="text-sm font-semibold mb-3">Registration</div>
|
|
||||||
<label class="flex items-center gap-3">
|
|
||||||
{% block registration_status_input %}
|
|
||||||
<form hx-patch="/toggle-registrations" hx-swap="outerHTML" hx-trigger="change">
|
|
||||||
<input name="registration_open" type="checkbox" class="nb-checkbox" {% if settings.registrations_enabled %}checked{% endif %} />
|
|
||||||
</form>
|
|
||||||
{% endblock %}
|
|
||||||
<span class="text-sm">Enable Registrations</span>
|
|
||||||
</label>
|
|
||||||
<div id="registration-status" class="text-xs opacity-70 mt-2"></div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
130
html-router/templates/admin/sections/models.html
Normal file
130
html-router/templates/admin/sections/models.html
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
<section class="nb-panel p-4 sm:p-5 flex flex-col gap-4">
|
||||||
|
<div class="flex items-start justify-between flex-col sm:flex-row gap-3">
|
||||||
|
<div>
|
||||||
|
<div class="text-sm uppercase tracking-wide opacity-60 mb-1">AI Models</div>
|
||||||
|
<h2 class="text-lg font-semibold">Model configuration</h2>
|
||||||
|
<p class="text-xs opacity-70 max-w-2xl">
|
||||||
|
Choose which models power conversational search, ingestion analysis, and embeddings. Adjusting embeddings may trigger a full reprocess.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
href="/admin?section=overview"
|
||||||
|
class="nb-btn btn-sm btn-ghost"
|
||||||
|
hx-boost="true"
|
||||||
|
hx-target="#admin-shell"
|
||||||
|
hx-select="#admin-shell"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
hx-push-url="true"
|
||||||
|
>
|
||||||
|
← Back to Admin
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if available_models %}
|
||||||
|
{% block model_settings_form %}
|
||||||
|
<form hx-patch="/update-model-settings" hx-swap="outerHTML" class="grid grid-cols-1 gap-4">
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<div class="text-sm opacity-80 mb-1">Query Model</div>
|
||||||
|
<select name="query_model" class="nb-select w-full">
|
||||||
|
{% for model in available_models.data %}
|
||||||
|
<option value="{{ model.id }}" {% if settings.query_model == model.id %}selected{% endif %}>{{ model.id }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<p class="text-xs opacity-70 mt-1">Current: <span class="font-mono">{{ settings.query_model }}</span></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="text-sm opacity-80 mb-1">Processing Model</div>
|
||||||
|
<select name="processing_model" class="nb-select w-full">
|
||||||
|
{% for model in available_models.data %}
|
||||||
|
<option value="{{ model.id }}" {% if settings.processing_model == model.id %}selected{% endif %}>{{ model.id }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<p class="text-xs opacity-70 mt-1">Current: <span class="font-mono">{{ settings.processing_model }}</span></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<div class="text-sm opacity-80 mb-1">Image Processing Model</div>
|
||||||
|
<select name="image_processing_model" class="nb-select w-full">
|
||||||
|
{% for model in available_models.data %}
|
||||||
|
<option value="{{ model.id }}" {% if settings.image_processing_model == model.id %}selected{% endif %}>{{ model.id }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<p class="text-xs opacity-70 mt-1">Current: <span class="font-mono">{{ settings.image_processing_model }}</span></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="text-sm opacity-80 mb-1">Voice Processing Model</div>
|
||||||
|
<select name="voice_processing_model" class="nb-select w-full">
|
||||||
|
{% for model in available_models.data %}
|
||||||
|
<option value="{{ model.id }}" {% if settings.voice_processing_model == model.id %}selected{% endif %}>{{ model.id }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<p class="text-xs opacity-70 mt-1">Current: <span class="font-mono">{{ settings.voice_processing_model }}</span></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<div class="text-sm opacity-80 mb-1">Embedding Model</div>
|
||||||
|
<select name="embedding_model" class="nb-select w-full">
|
||||||
|
{% for model in available_models.data %}
|
||||||
|
<option value="{{ model.id }}" {% if settings.embedding_model == model.id %}selected{% endif %}>{{ model.id }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<p class="text-xs opacity-70 mt-1">Current: <span class="font-mono">{{ settings.embedding_model }}</span></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="text-sm opacity-80 mb-1" for="embedding_dimensions">Embedding Dimensions</div>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="embedding_dimensions"
|
||||||
|
name="embedding_dimensions"
|
||||||
|
class="nb-input w-full"
|
||||||
|
value="{{ settings.embedding_dimensions }}"
|
||||||
|
required
|
||||||
|
min="1"
|
||||||
|
/>
|
||||||
|
<p class="text-xs opacity-70 mt-1">Changing dimensions will trigger a background re-embedding.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="embedding-change-alert" class="nb-panel p-3 bg-warning/20 hidden">
|
||||||
|
<div class="text-sm">
|
||||||
|
<strong>Warning:</strong> Changing dimensions recreates embeddings for text chunks and knowledge entities. Confirm the target model requires the new value.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<button type="submit" class="nb-btn nb-cta btn-sm">Save Model Settings</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(() => {
|
||||||
|
const dimensionInput = document.getElementById('embedding_dimensions');
|
||||||
|
const alertElement = document.getElementById('embedding-change-alert');
|
||||||
|
const initialDimensions = '{{ settings.embedding_dimensions }}';
|
||||||
|
if (dimensionInput && alertElement) {
|
||||||
|
dimensionInput.addEventListener('input', (event) => {
|
||||||
|
if (String(event.target.value) !== String(initialDimensions)) {
|
||||||
|
alertElement.classList.remove('hidden');
|
||||||
|
} else {
|
||||||
|
alertElement.classList.add('hidden');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
{% else %}
|
||||||
|
<div class="nb-panel p-4 bg-warning/10 border border-warning/40">
|
||||||
|
<div class="text-sm font-semibold mb-1">Unable to load models</div>
|
||||||
|
<p class="text-xs opacity-70">We could not reach the model provider. Check the API key and retry.</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</section>
|
||||||
57
html-router/templates/admin/sections/overview.html
Normal file
57
html-router/templates/admin/sections/overview.html
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
{% if analytics %}
|
||||||
|
<section class="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||||
|
<div class="nb-stat">
|
||||||
|
<div class="text-xs opacity-70">Page Loads</div>
|
||||||
|
<div class="text-3xl font-extrabold">{{ analytics.page_loads }}</div>
|
||||||
|
<div class="text-xs opacity-60">Total load events seen by Minne</div>
|
||||||
|
</div>
|
||||||
|
<div class="nb-stat">
|
||||||
|
<div class="text-xs opacity-70">Unique Visitors</div>
|
||||||
|
<div class="text-3xl font-extrabold">{{ analytics.visitors }}</div>
|
||||||
|
<div class="text-xs opacity-60">Distinct users by fingerprint</div>
|
||||||
|
</div>
|
||||||
|
<div class="nb-stat">
|
||||||
|
<div class="text-xs opacity-70">Users</div>
|
||||||
|
<div class="text-3xl font-extrabold">{{ users or 0 }}</div>
|
||||||
|
<div class="text-xs opacity-60">Registered accounts</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% else %}
|
||||||
|
<section class="nb-panel p-4">
|
||||||
|
<div class="text-sm font-semibold mb-2">Analytics unavailable</div>
|
||||||
|
<p class="text-xs opacity-70">We could not fetch analytics for this view. Reload or check the monitoring pipeline.</p>
|
||||||
|
</section>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<section class="grid grid-cols-1 xl:grid-cols-2 gap-4">
|
||||||
|
{% block system_prompt_section %}
|
||||||
|
<div id="system_prompt_section" class="nb-panel p-4">
|
||||||
|
<div class="flex items-start justify-between gap-2 mb-3">
|
||||||
|
<div>
|
||||||
|
<div class="text-sm font-semibold">System Prompts</div>
|
||||||
|
<p class="text-xs opacity-70">Adjust the prompts that power retrieval, ingestion analysis, and image processing flows.</p>
|
||||||
|
</div>
|
||||||
|
<span class="text-[10px] uppercase tracking-wide opacity-60">LLM</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2 flex-col sm:flex-row">
|
||||||
|
<button type="button" class="nb-btn btn-sm" hx-get="/edit-query-prompt" hx-target="#modal" hx-swap="innerHTML">Edit Query Prompt</button>
|
||||||
|
<button type="button" class="nb-btn btn-sm" hx-get="/edit-ingestion-prompt" hx-target="#modal" hx-swap="innerHTML">Edit Ingestion Prompt</button>
|
||||||
|
<button type="button" class="nb-btn btn-sm" hx-get="/edit-image-prompt" hx-target="#modal" hx-swap="innerHTML">Edit Image Prompt</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
<div class="nb-panel p-4">
|
||||||
|
<div class="text-sm font-semibold mb-2">Registration</div>
|
||||||
|
<p class="text-xs opacity-60 mb-3">Toggle whether new people can sign up without an invite.</p>
|
||||||
|
<label class="flex items-center gap-3">
|
||||||
|
{% block registration_status_input %}
|
||||||
|
<form hx-patch="/toggle-registrations" hx-swap="outerHTML" hx-trigger="change">
|
||||||
|
<input name="registration_open" type="checkbox" class="nb-checkbox" {% if settings.registrations_enabled %}checked{% endif %} />
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
|
<span class="text-sm">Enable Registrations</span>
|
||||||
|
</label>
|
||||||
|
<div id="registration-status" class="text-xs opacity-70 mt-2"></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
Reference in New Issue
Block a user