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
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:
176
server/web/src/components/CalibrationAIControl.tsx
Normal file
176
server/web/src/components/CalibrationAIControl.tsx
Normal 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 & 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user