feat(studio): in-place System AI editor (v0.12.0)

Replaces the read-only System AI block in Settings with a real
editor. Operator never has to touch ~/.fai/config.yaml again
to change provider, endpoint, model, API-key env-var, or
privacy mode.

UX choices that align with the zero-learning-curve rule:

  * Provider dropdown lists Ollama / OpenAI / LM Studio / vLLM
    / Custom by friendly name. Each selection auto-fills the
    endpoint and api_key_env defaults — but only when the
    field is still empty or matches a different preset's
    default, so manual overrides are never clobbered.

  * Each provider has a one-line description rendered under
    the dropdown ("Ollama → Local `ollama serve`. Models stay
    on your machine. No API key needed.") and a model-hint
    placeholder ("gemma3:4b · llama3.2:3b · qwen2.5-coder:7b")
    that goes away when a model is typed.

  * Privacy mode is a vertical radio group with each option's
    description in place — no doc-lookup needed.

  * "Test connection" sends an `ok`-ping and renders the
    same error-kind → fix-hint mapping the audit drill-down
    uses.

  * "Save" persists + hot-reloads the hub. No daemon restart
    required. UI status badge flips to "enabled · <privacy>"
    immediately.

`fai_dart_sdk` 0.5.0 carries the underlying `updateSystemAi` /
`testSystemAi` RPCs.

Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
flemming-it 2026-05-07 13:41:11 +02:00
parent 4c4e2bb1eb
commit 15ea403c86
6 changed files with 601 additions and 5 deletions

View file

@ -9,6 +9,7 @@ import '../data/hub.dart';
import '../theme/theme.dart';
import '../theme/tokens.dart';
import 'fai_pill.dart';
import 'fai_system_ai_editor.dart';
class FaiSettingsDialog extends StatefulWidget {
const FaiSettingsDialog({super.key});
@ -236,7 +237,18 @@ class _FaiSettingsDialogState extends State<FaiSettingsDialog> {
],
if (_aiStatus != null) ...[
const SizedBox(height: FaiSpace.lg),
_SystemAiPanel(status: _aiStatus!),
_SystemAiPanel(
status: _aiStatus!,
onEdit: () async {
final updated = await FaiSystemAiEditor.show(
context,
_aiStatus!,
);
if (updated != null && mounted) {
setState(() => _aiStatus = updated);
}
},
),
],
],
),
@ -346,8 +358,9 @@ class _ChannelRow extends StatelessWidget {
class _SystemAiPanel extends StatelessWidget {
final SystemAiStatus status;
final VoidCallback onEdit;
const _SystemAiPanel({required this.status});
const _SystemAiPanel({required this.status, required this.onEdit});
@override
Widget build(BuildContext context) {
@ -379,6 +392,12 @@ class _SystemAiPanel extends StatelessWidget {
: FaiPillTone.neutral,
),
],
const Spacer(),
TextButton.icon(
onPressed: onEdit,
icon: const Icon(Icons.edit_outlined, size: 14),
label: Text(status.enabled ? 'Edit…' : 'Configure…'),
),
],
),
const SizedBox(height: 4),
@ -392,7 +411,10 @@ class _SystemAiPanel extends StatelessWidget {
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.',
'Off — failure explanations on the audit page are disabled. '
'Click Configure… to pick a provider (Ollama / OpenAI / LM '
'Studio / vLLM / Custom). Operator config is rewritten in place; '
'no daemon restart needed.',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),