Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
229 changes: 166 additions & 63 deletions humux/api/admin.py

Large diffs are not rendered by default.

86 changes: 55 additions & 31 deletions humux/api/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -284,8 +284,15 @@
url.searchParams.set('llmsub', s);
history.replaceState(history.state, '', url);
} catch (e) {}
this._loadSection(s);
},
reloadLlmPartial() { if (window.htmx) { htmx.ajax('GET', '/partials/llm', {target: '#tab-content', swap: 'innerHTML'}); } },
_loadSection(s) {
if (window.htmx) {
showSkeleton('#llm-sub');
htmx.ajax('GET', '/partials/llm/' + s, {target: '#llm-sub', swap: 'innerHTML'});
}
},
reloadLlmPartial() { this._loadSection(this.section); },
providerOptions: window.LLM_PROVIDERS,
provider: currentProvider,
apiKey: apiKey || '',
Expand Down Expand Up @@ -549,6 +556,8 @@
this.resetMemoryModel('consolidation');
this.resetBackgroundModel('gd');
this.resetBackgroundModel('tr');
// Load the initial sub-tab content after seeding model values.
this._loadSection(this.section);
});
},
resetModel() {
Expand Down Expand Up @@ -1319,41 +1328,56 @@
}
window.accountsTabs = accountsTabs;

// Vaults tab: Agents | Infra | Import. Unlike inspect/accounts these are
// shown/hidden in place (x-show) because every action re-renders the whole
// partial into #tab-content; the sub-tab is mirrored in the URL (?vaultsub=)
// and restored after each re-render.
// Vaults tab: Agents | Infra | Import sub-tabs.
// Persisted via ?vaultsub= URL param; each sub-tab lazy-loads its own
// partial via htmx with skeleton loading.
function vaultTabs() {
const allowed = ['agents', 'infra', 'import'];
return {
sub: (() => {
try {
const q = new URLSearchParams(window.location.search).get('vaultsub');
if (allowed.includes(q)) { return q; }
const ls = localStorage.getItem('vault_sub');
if (allowed.includes(ls)) { return ls; }
} catch (e) {}
return 'agents';
})(),
init() { this._syncUrl(this.sub); },
select(name) {
this.sub = name;
try { localStorage.setItem('vault_sub', name); } catch (e) {}
this._syncUrl(name);
return subTabs({
subs: {
agents: '/partials/secrets/agents',
infra: '/partials/secrets/infra',
import: '/partials/secrets/import'
},
_syncUrl(name) {
try {
const url = new URL(window.location.href);
if (url.searchParams.get('vaultsub') !== name) {
url.searchParams.set('vaultsub', name);
history.replaceState(history.state, '', url);
}
} catch (e) {}
}
};
param: 'vaultsub',
storageKey: 'vault_sub',
container: '#vault-sub',
fallback: 'agents'
});
}
window.vaultTabs = vaultTabs;

// Memory tab: Settings | Long-term | Short-term sub-tabs.
// Persisted via ?memsub= URL param; each sub-tab lazy-loads its own
// partial via htmx with skeleton loading.
function memorySubtab() {
return subTabs({
subs: {
settings: '/partials/memory/settings',
'long-term': '/partials/memory/long-term',
'short-term': '/partials/memory/short-term'
},
param: 'memsub',
storageKey: 'memory_sub',
container: '#memory-sub',
fallback: 'settings'
});
}
window.memorySubtab = memorySubtab;

// Jobs tab: Scheduled | Subagents sub-tabs.
// Persisted via ?jobsub= URL param; each sub-tab lazy-loads its own
// partial via htmx with skeleton loading.
function jobsTabs() {
return subTabs({
subs: { scheduled: '/partials/jobs/scheduled', subagents: '/partials/jobs/subagents' },
param: 'jobsub',
storageKey: 'jobs_sub',
container: '#jobs-sub',
fallback: 'scheduled'
});
}
window.jobsTabs = jobsTabs;

// Global: handle 401 by redirecting to auth gate
document.body.addEventListener('htmx:responseError', function(e) {
if (e.detail.xhr.status === 401) {
Expand Down
84 changes: 84 additions & 0 deletions humux/api/templates/partials/_macros.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
{# Shared Jinja macros used by multiple tab partials #}

{# ── LLM thinking-level control ─────────────────────────────── #}
{%- macro think(level, prov, model, lid, bg=false) -%}
<div class="mt-2">
<label class="label" for="{{ lid }}-input">Thinking level{% if bg %}<span class="text-muted text-xs font-normal" x-show="sameAsMain({{ prov }}, {{ model }})"> · same config as Main inference</span>{% endif %}</label>
<div class="flex items-center gap-2">
<input type="text" class="input-sm" style="max-width:300px" x-model="{{ level }}"
list="{{ lid }}" id="{{ lid }}-input" placeholder="off — type or pick low / medium / high">
<datalist id="{{ lid }}">
<option value=""></option>
<template x-for="lvl in levelOptions({{ prov }})" :key="'{{ lid }}-' + lvl">
<option :value="lvl"></option>
</template>
</datalist>
<button type="button" class="btn-secondary btn-sm" x-show="{{ prov }} === 'anthropic'"
@click="fetchThinkingLevels({{ prov }}, {{ model }})" :disabled="fetchingLevels[{{ prov }}]">
<span x-show="!fetchingLevels[{{ prov }}]">Fetch levels</span>
<span x-show="fetchingLevels[{{ prov }}]"><span class="spinner-sm mr-1"></span>…</span>
</button>
<a class="text-xs text-muted underline" x-show="{{ prov }} !== 'anthropic'"
:href="providerDocsUrl({{ prov }})" target="_blank" rel="noopener">effort docs ↗</a>
</div>
<p class="text-muted text-xs mt-1" x-show="fetchLevelsResult[{{ prov }}]" x-text="fetchLevelsResult[{{ prov }}]"></p>
<p class="text-muted text-xs mt-1">Leave <strong>Off</strong> for non-reasoning models. Higher = more reasoning, more tokens. Anthropic: "Fetch levels" autodiscovers supported values; other providers: see the docs link and type the value.</p>
<p class="text-amber-600 text-xs mt-1" x-show="{{ level }} && !modelSupportsThinking({{ model }})">Heads up: "<span x-text="{{ model }}"></span>" isn't a recognized reasoning model — only set a level if you know it supports thinking, or the inference call may error.</p>
</div>
{%- endmacro %}

{# ── Memory pagination ──────────────────────────────────────── #}
{%- macro memory_pag(view, total, suffix='') %}
{% set off = memory_offset|default(0)|int %}
{% set lim = memory_limit|default(25)|int %}
{% set q = memory_q|default('') %}
{% set page_sizes = [25, 50, 100] %}
{% set total = total|default(0)|int %}
{% set cp = (off // lim) + 1 if total > 0 else 1 %}
{% set tp = ((total + lim - 1) // lim) if total > 0 else 1 %}
{% set ss = off + 1 if total > 0 else 0 %}
{% set se = [(off + lim), total]|min if total > 0 else 0 %}
{% set qs = ('&q=' + q|urlencode) if q else '' %}
{% set pid = 'ps-' + view + suffix %}
<div class="flex items-center justify-between gap-2 mb-3 flex-wrap">
<div class="flex items-center gap-2">
<span class="text-xs text-muted">
{%- if total > 0 %}Showing {{ ss }}&ndash;{{ se }} of {{ total }}{% else %}No entries{% endif -%}
</span>
<label class="label mb-0 text-xs" for="{{ pid }}">Per page</label>
<select class="input-sm" style="max-width:80px" id="{{ pid }}"
onchange="htmx.ajax('GET','/partials/memory/{{ view }}?offset=0&limit='+this.value+'{{ qs }}',{target:'#memory-sub',swap:'innerHTML'})">
{% for ps in page_sizes %}<option value="{{ ps }}"{% if lim == ps %} selected{% endif %}>{{ ps }}</option>{% endfor %}
</select>
</div>
<div class="flex items-center gap-1">
<button class="btn btn-sm"{% if cp <= 1 %} disabled{% endif %}
hx-get="/partials/memory/{{ view }}?offset=0&limit={{ lim }}{{ qs }}" hx-target="#memory-sub" hx-swap="innerHTML">&laquo;</button>
<button class="btn btn-sm"{% if cp <= 1 %} disabled{% endif %}
hx-get="/partials/memory/{{ view }}?offset={{ [off - lim, 0]|max }}&limit={{ lim }}{{ qs }}" hx-target="#memory-sub" hx-swap="innerHTML">&lsaquo;</button>
<span class="text-xs text-muted px-1">Page {{ cp }} / {{ tp }}</span>
<button class="btn btn-sm"{% if cp >= tp %} disabled{% endif %}
hx-get="/partials/memory/{{ view }}?offset={{ off + lim }}&limit={{ lim }}{{ qs }}" hx-target="#memory-sub" hx-swap="innerHTML">&rsaquo;</button>
<button class="btn btn-sm"{% if cp >= tp %} disabled{% endif %}
hx-get="/partials/memory/{{ view }}?offset={{ (tp - 1) * lim }}&limit={{ lim }}{{ qs }}" hx-target="#memory-sub" hx-swap="innerHTML">&raquo;</button>
</div>
</div>
{%- endmacro %}

{# ── Memory search ──────────────────────────────────────────── #}
{%- macro memory_search(view) %}
{% set off = memory_offset|default(0)|int %}
{% set lim = memory_limit|default(25)|int %}
{% set q = memory_q|default('') %}
<div class="flex items-center gap-2 mb-3">
<input type="text" class="input-sm flex-1" style="max-width:300px" placeholder="Search memories..."
value="{{ q }}" id="search-{{ view }}"
onkeydown="if(event.key==='Enter'){const v=this.value;htmx.ajax('GET','/partials/memory/{{ view }}?offset=0&limit={{ lim }}'+(v?'&q='+encodeURIComponent(v):''),{target:'#memory-sub',swap:'innerHTML'});}">
<button class="btn btn-sm"
hx-get="/partials/memory/{{ view }}?offset=0&limit={{ lim }}" hx-target="#memory-sub" hx-swap="innerHTML"
title="Refresh">&#x21bb;</button>
</div>
{%- endmacro %}

{# ── Memory category list ───────────────────────────────────── #}
{% set memory_categories = ['preference','relationship','fact','routine','work','health','travel'] %}
Loading
Loading