feat(studio): "Explain this" + System AI in Settings (v0.11.0)

Two new operator affordances powered by `HubAdmin.AskAi`:

  * **Audit drill-down → "Explain" button.** Tappable on every
    failed event when System AI is configured; greyed out with
    a "Configure in Settings" tooltip otherwise. Shows an
    inline panel under the event fields with the LLM's plain-
    language explanation, a "FIX" suggestion, the privacy-mode
    badge (`redacted` / `full`), and round-trip latency. Errors
    map to actionable fix hints (`env_missing` → "set $VAR in
    your shell, then fai daemon restart"; `network` → "is your
    provider running?"; etc.) — no raw HTTP errors surfaced.

  * **Settings dialog → System AI panel.** Renders the live
    HubAdmin.SystemAiStatus (provider, endpoint, model,
    api_key_env name, privacy mode). When disabled, shows a
    one-line explainer + pointer to docs/architecture/system-ai.md.
    Read-only for now; editing the config still requires
    ~/.fai/config.yaml (Phase-1+ adds in-place editing).

Both surfaces follow the new zero-learning-curve UX rule:
the user never has to look up what the configurable means or
how to fix an error — every state self-explains in place.

Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
flemming-it 2026-05-07 13:18:22 +02:00
parent 060c8003d5
commit 4c4e2bb1eb
6 changed files with 415 additions and 4 deletions

View file

@ -106,6 +106,35 @@ class HubService {
);
}
/// System-AI status snapshot used by the Settings dialog
/// and the audit drill-down to decide whether the "Explain"
/// affordance is active. Always returns a struct (no
/// exceptions on disabled state).
Future<SystemAiStatus> systemAiStatus() async {
final r = await _client.systemAiStatus();
return SystemAiStatus(
enabled: r.enabled,
provider: r.provider,
endpoint: r.endpoint,
model: r.model,
privacyMode: r.privacyMode,
apiKeyEnv: r.apiKeyEnv,
);
}
/// Send a one-shot prompt to the System AI. Returns the
/// answer + latency on success or an [AskAiResult] with
/// `errorKind` set on failure (never throws). Studio maps
/// the error kind to its inline-fix copy.
Future<AskAiResult> askAi(String prompt) async {
final r = await _client.askAi(prompt);
return AskAiResult(
errorKind: r.errorKind,
text: r.text,
latencyMs: r.latencyMs,
);
}
/// Active channel + per-channel daemon status snapshot.
Future<ChannelStatusSnapshot> channelStatus() async {
final r = await _client.channelStatus();
@ -498,6 +527,67 @@ class PendingApproval {
});
}
class SystemAiStatus {
final bool enabled;
final String provider;
final String endpoint;
final String model;
/// off / redacted / full.
final String privacyMode;
final String apiKeyEnv;
const SystemAiStatus({
required this.enabled,
required this.provider,
required this.endpoint,
required this.model,
required this.privacyMode,
required this.apiKeyEnv,
});
}
class AskAiResult {
/// "" on success; otherwise one of:
/// disabled / env_missing / network / http / parse / empty_response.
final String errorKind;
/// On success: the assistant's reply.
/// On failure: a human-readable diagnostic.
final String text;
/// Round-trip latency in ms (success only).
final int latencyMs;
const AskAiResult({
required this.errorKind,
required this.text,
required this.latencyMs,
});
bool get isSuccess => errorKind.isEmpty;
/// One-line fix hint per error kind, mirroring
/// `docs/architecture/system-ai.md`. Empty on success.
String get fixHint {
switch (errorKind) {
case '':
return '';
case 'disabled':
return 'Configure System AI in Settings (Cmd+,) → System AI panel.';
case 'env_missing':
return 'The API-key env var is empty. `export <var>=<key>` in your shell, then `fai daemon restart`.';
case 'network':
return 'Hub cannot reach the configured endpoint. Is your provider running?';
case 'http':
return 'Provider returned a non-2xx status. Verify endpoint, model name, and API key.';
case 'parse':
return 'Provider returned an unexpected response shape. Non-OpenAI providers may need a compatibility proxy.';
case 'empty_response':
return 'Provider answered with no content. Try a different model or rephrase the prompt.';
default:
return 'Unknown failure kind ($errorKind). See system-ai.md.';
}
}
}
class ChannelInfo {
final String name;
final int port;