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:
parent
060c8003d5
commit
4c4e2bb1eb
6 changed files with 415 additions and 4 deletions
|
|
@ -8,6 +8,7 @@ import 'package:flutter/material.dart';
|
|||
import '../data/hub.dart';
|
||||
import '../theme/theme.dart';
|
||||
import '../theme/tokens.dart';
|
||||
import 'fai_pill.dart';
|
||||
|
||||
class FaiSettingsDialog extends StatefulWidget {
|
||||
const FaiSettingsDialog({super.key});
|
||||
|
|
@ -32,6 +33,7 @@ class _FaiSettingsDialogState extends State<FaiSettingsDialog> {
|
|||
bool _saving = false;
|
||||
String? _error;
|
||||
ChannelStatusSnapshot? _channels;
|
||||
SystemAiStatus? _aiStatus;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -41,6 +43,7 @@ class _FaiSettingsDialogState extends State<FaiSettingsDialog> {
|
|||
_port = TextEditingController(text: ep.port.toString());
|
||||
_secure = ep.secure;
|
||||
_loadChannels();
|
||||
_loadAiStatus();
|
||||
}
|
||||
|
||||
Future<void> _loadChannels() async {
|
||||
|
|
@ -55,6 +58,16 @@ class _FaiSettingsDialogState extends State<FaiSettingsDialog> {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> _loadAiStatus() async {
|
||||
try {
|
||||
final s = await HubService.instance.systemAiStatus();
|
||||
if (!mounted) return;
|
||||
setState(() => _aiStatus = s);
|
||||
} catch (_) {
|
||||
// Same fail-quiet rule as channels.
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _connectToChannel(ChannelInfo ch) async {
|
||||
setState(() {
|
||||
_host.text = '127.0.0.1';
|
||||
|
|
@ -221,6 +234,10 @@ class _FaiSettingsDialogState extends State<FaiSettingsDialog> {
|
|||
onConnect: _saving ? null : () => _connectToChannel(ch),
|
||||
),
|
||||
],
|
||||
if (_aiStatus != null) ...[
|
||||
const SizedBox(height: FaiSpace.lg),
|
||||
_SystemAiPanel(status: _aiStatus!),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
@ -326,3 +343,105 @@ class _ChannelRow extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SystemAiPanel extends StatelessWidget {
|
||||
final SystemAiStatus status;
|
||||
|
||||
const _SystemAiPanel({required this.status});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'SYSTEM AI',
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
letterSpacing: 0.6,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: FaiSpace.sm),
|
||||
FaiPill(
|
||||
label: status.enabled ? 'enabled' : 'off',
|
||||
tone: status.enabled ? FaiPillTone.success : FaiPillTone.neutral,
|
||||
),
|
||||
if (status.enabled) ...[
|
||||
const SizedBox(width: FaiSpace.xs),
|
||||
FaiPill(
|
||||
label: status.privacyMode,
|
||||
tone: status.privacyMode == 'full'
|
||||
? FaiPillTone.warning
|
||||
: FaiPillTone.neutral,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
if (status.enabled) ...[
|
||||
_StatRow(label: 'provider', value: status.provider),
|
||||
_StatRow(label: 'endpoint', value: status.endpoint, mono: true),
|
||||
_StatRow(label: 'model', value: status.model, mono: true),
|
||||
if (status.apiKeyEnv.isNotEmpty)
|
||||
_StatRow(label: 'api_key_env', value: '\$${status.apiKeyEnv}', mono: true),
|
||||
] else ...[
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 2),
|
||||
child: Text(
|
||||
'Off — explanations on failed events are disabled. Add a `system_llm:` block to ~/.fai/config.yaml and restart the daemon. See docs/architecture/system-ai.md.',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _StatRow extends StatelessWidget {
|
||||
final String label;
|
||||
final String value;
|
||||
final bool mono;
|
||||
|
||||
const _StatRow({
|
||||
required this.label,
|
||||
required this.value,
|
||||
this.mono = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 1),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 96,
|
||||
child: Text(
|
||||
label,
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value.isEmpty ? '—' : value,
|
||||
style: mono
|
||||
? FaiTheme.mono(size: 11, color: theme.colorScheme.onSurface)
|
||||
: theme.textTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue