Add Calibrate AI Control UI and fleet LLM backend wiring.
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled

Operators toggle Logic gates vs AI Control on Settings, refresh local Ollama models, and save ai_endpoint settings via Calibrate PUT; server scheduler and agent snapshot/command paths support stateless 60s fleet decisions.
This commit is contained in:
AetherForge
2026-06-07 02:14:28 -07:00
parent 34afa28f81
commit 0002e5fd93
33 changed files with 2791 additions and 12 deletions

View File

@@ -0,0 +1,176 @@
import { useState } from 'react';
import type { ServerSettings } from '../types';
import { api } from '../api/client';
import { HelpTip, FieldHint } from './HelpTip';
import { ADAPTIVE_STRATEGY_HELP } from '../help/lotlOnionTiers';
export const DEFAULT_AI_LOCAL_ENDPOINT = 'http://127.0.0.1:11434/v1';
export const DEFAULT_AI_INTERVAL_SEC = 60;
function readEndpoint(server: ServerSettings): string {
return server.ai_endpoint?.trim() || server.ai_local_endpoint?.trim() || DEFAULT_AI_LOCAL_ENDPOINT;
}
function readIntervalSec(server: ServerSettings): number {
return server.ai_decision_interval_sec ?? server.ai_interval_sec ?? DEFAULT_AI_INTERVAL_SEC;
}
interface Props {
server: ServerSettings;
onUpdate: (path: string, value: unknown) => void;
}
export default function CalibrationAIControl({ server, onUpdate }: Props) {
const aiControl = server.ai_control_enabled ?? false;
const endpoint = readEndpoint(server);
const model = server.ai_model?.trim() || '';
const intervalSec = readIntervalSec(server);
const [models, setModels] = useState<string[]>([]);
const [refreshing, setRefreshing] = useState(false);
const [modelsMsg, setModelsMsg] = useState('');
const handleRefreshModels = async () => {
setRefreshing(true);
setModelsMsg('');
try {
const res = await api.getAIModels(endpoint);
setModels(res.models ?? []);
if (!res.models?.length) {
setModelsMsg(res.error || 'No models returned — is Ollama running?');
} else {
setModelsMsg(`${res.models.length} model(s) loaded`);
if (!model && res.models[0]) {
onUpdate('server.ai_model', res.models[0]);
}
}
} catch (e) {
setModels([]);
setModelsMsg(e instanceof Error ? e.message : 'Failed to refresh models');
} finally {
setRefreshing(false);
}
};
return (
<div className="calibration-ai-control">
<div
className="calibration-mode-toggle"
role="group"
aria-label="Calibration control mode"
>
<button
type="button"
className={`calibration-mode-btn ${!aiControl ? 'calibration-mode-btn--active' : ''}`}
aria-pressed={!aiControl}
onClick={() => onUpdate('server.ai_control_enabled', false)}
>
<span className="calibration-mode-label">Logic gates</span>
<span className="calibration-mode-sub">Adaptive strategy &amp; tier chains</span>
</button>
<button
type="button"
className={`calibration-mode-btn ${aiControl ? 'calibration-mode-btn--active' : ''}`}
aria-pressed={aiControl}
onClick={() => onUpdate('server.ai_control_enabled', true)}
>
<span className="calibration-mode-label">AI Control</span>
<span className="calibration-mode-sub">Local LLM fleet decisions</span>
</button>
</div>
{aiControl ? (
<div className="calibration-ai-panel">
<p className="section-desc calibration-ai-blurb">
Fleet AI issues <strong>stateless</strong> decisions every {intervalSec}s per agent no memory
between cycles. The control server calls your local LLM; agents execute tool calls on{' '}
<em>your</em> machines only. Complete fleet control stays on your LAN.
</p>
<div className="form-group">
<label htmlFor="cal-ai-endpoint" className="label">
Local API URL <HelpTip field="ai_local_endpoint" />
</label>
<input
id="cal-ai-endpoint"
type="url"
className="input mono"
value={endpoint}
placeholder={DEFAULT_AI_LOCAL_ENDPOINT}
onChange={(e) => onUpdate('server.ai_endpoint', e.target.value)}
/>
<FieldHint field="ai_local_endpoint" />
</div>
<div className="form-row calibration-ai-model-row">
<button
type="button"
className="btn btn-outline btn-sm"
disabled={refreshing}
onClick={handleRefreshModels}
>
{refreshing ? 'Refreshing…' : 'Refresh models'}
</button>
<div className="form-group" style={{ flex: 1, margin: 0 }}>
<label htmlFor="cal-ai-model" className="label">
Model <HelpTip field="calibration_ai_model" />
</label>
<select
id="cal-ai-model"
className="input"
value={model}
onChange={(e) => onUpdate('server.ai_model', e.target.value)}
>
<option value="">{models.length ? 'Select a model…' : 'Refresh models first'}</option>
{model && !models.includes(model) && (
<option value={model}>{model}</option>
)}
{models.map((m) => (
<option key={m} value={m}>{m}</option>
))}
</select>
</div>
</div>
{modelsMsg && (
<p className="form-hint calibration-ai-models-msg">{modelsMsg}</p>
)}
<div className="calibration-ai-meta">
<div className="calibration-ai-info-chip">
<span className="font-tech">ai_no_context</span>
<span className="calibration-ai-info-value">always on</span>
<HelpTip field="ai_no_context" />
</div>
<div className="calibration-ai-info-chip">
<span className="font-tech">Interval</span>
<span className="calibration-ai-info-value">{intervalSec}s per agent</span>
<HelpTip field="ai_interval_sec" />
</div>
</div>
</div>
) : (
<div className="calibration-logic-panel">
<p className="section-desc">{ADAPTIVE_STRATEGY_HELP}</p>
<FieldHint field="adaptive_strategy" />
<FieldHint field="lotl_onion_tiers" />
<div className="form-group checkbox-group" style={{ marginTop: '1rem' }}>
<label className="checkbox-label">
<input
type="checkbox"
className="checkbox"
checked={server.adaptive_strategy_enabled !== false}
onChange={(e) => onUpdate('server.adaptive_strategy_enabled', e.target.checked)}
/>
<span>Enable adaptive strategy engine <HelpTip field="adaptive_strategy" /></span>
</label>
</div>
{server.lotl_onion_tiers?.length ? (
<p className="form-hint" style={{ marginTop: '0.75rem' }}>
Spread tier order: <code className="mono-sm">{server.lotl_onion_tiers.join(' → ')}</code>
</p>
) : null}
</div>
)}
</div>
);
}