// Guided-setup wizard. Collects scenario / intent / target (plus two // adaptive toggles), then calls `chain init --answers` to PREVIEW the // assembled plan and `--apply` to write the config. All the policy lives // in the Rust deterministic engine (chain_core::guided_setup) — this is // a thin UI over the CLI, so the logic never diverges. // // NOTE: the dropdown option labels are humanised from the canonical // kebab-case enum values (trying-out → "Trying Out"); localising those // per option is a follow-up. The structural UI + the applied plan // (rendered from the CLI's localisable output) are the meaningful parts. import 'dart:io'; import 'package:flutter/material.dart'; import '../data/error_presentation.dart'; import '../data/system_actions.dart'; import '../l10n/app_localizations.dart'; class GuidedSetupDialog extends StatefulWidget { const GuidedSetupDialog({super.key}); /// Launch the wizard as a modal dialog. static Future show(BuildContext context) => showDialog( context: context, builder: (_) => const GuidedSetupDialog(), ); @override State createState() => _GuidedSetupDialogState(); } class _GuidedSetupDialogState extends State { // Canonical SetupAnswers enum values (serde kebab-case in Rust). static const _scenarios = [ 'trying-out', 'team-hub', 'regulated-production', 'building-modules', ]; static const _intents = [ 'hello-world', 'extract-text', 'extract-summarize', 'classify-documents', 'build-module', ]; static const _targets = [ 'this-laptop', 'home-server', 'air-gapped-server', 'container', ]; int _step = 0; // 0 = answers, 1 = review/apply String _scenario = 'trying-out'; String _intent = 'extract-summarize'; String _target = 'this-laptop'; bool _requireApproval = false; bool _dataLocal = false; bool _busy = false; String? _preview; String? _applied; String _humanize(String kebab) => kebab .split('-') .map((w) => w.isEmpty ? w : '${w[0].toUpperCase()}${w.substring(1)}') .join(' '); String _answersYaml() => 'scenario: $_scenario\n' 'intent: $_intent\n' 'target: $_target\n' 'require_approval: $_requireApproval\n' 'data_must_stay_local: $_dataLocal\n'; Future _writeAnswers() async { final f = File('${Directory.systemTemp.path}/chain-setup-answers.yaml'); await f.writeAsString(_answersYaml()); return f.path; } Future _goReview() async { setState(() { _busy = true; _preview = null; }); final path = await _writeAnswers(); final r = await SystemActions.chainInit(['--answers', path]); if (!mounted) return; setState(() => _busy = false); if (r.ok) { setState(() { _preview = r.stdout.trim(); _step = 1; }); } else { showFaiProcessError(context, 'chain init --answers', r.stdout, r.stderr); } } Future _apply() async { setState(() => _busy = true); final path = await _writeAnswers(); final r = await SystemActions.chainInit( ['--answers', path, '--apply', '--force'], ); if (!mounted) return; setState(() => _busy = false); if (r.ok) { setState(() => _applied = r.stdout.trim()); } else { showFaiProcessError(context, 'chain init --apply', r.stdout, r.stderr); } } @override Widget build(BuildContext context) { final l = AppLocalizations.of(context)!; return AlertDialog( title: Text(l.guidedSetupTitle), content: SizedBox( width: 460, child: SingleChildScrollView( child: _step == 0 ? _answersStep(l) : _reviewStep(l), ), ), actions: _actions(l), ); } Widget _answersStep(AppLocalizations l) { return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(l.guidedSetupIntro), const SizedBox(height: 16), _dropdown(l.guidedSetupScenario, _scenarios, _scenario, (v) => setState(() => _scenario = v)), const SizedBox(height: 12), _dropdown(l.guidedSetupIntent, _intents, _intent, (v) => setState(() => _intent = v)), const SizedBox(height: 12), _dropdown(l.guidedSetupTarget, _targets, _target, (v) => setState(() => _target = v)), const SizedBox(height: 8), SwitchListTile( contentPadding: EdgeInsets.zero, title: Text(l.guidedSetupApproval), value: _requireApproval, onChanged: (v) => setState(() => _requireApproval = v), ), SwitchListTile( contentPadding: EdgeInsets.zero, title: Text(l.guidedSetupDataLocal), value: _dataLocal, onChanged: (v) => setState(() => _dataLocal = v), ), ], ); } Widget _dropdown( String label, List items, String value, ValueChanged onChanged, ) { return InputDecorator( decoration: InputDecoration( labelText: label, border: const OutlineInputBorder(), isDense: true, ), child: DropdownButtonHideUnderline( child: DropdownButton( isExpanded: true, value: value, items: [ for (final i in items) DropdownMenuItem(value: i, child: Text(_humanize(i))), ], onChanged: (v) { if (v != null) onChanged(v); }, ), ), ); } Widget _reviewStep(AppLocalizations l) { final applied = _applied; return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(applied != null ? l.guidedSetupApplied : l.guidedSetupReviewHeading), const SizedBox(height: 12), Container( width: double.infinity, padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Theme.of(context).colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(6), ), // SelectableText keeps the plan copyable (project rule: // anything an operator might want to keep is selectable). child: SelectableText( applied ?? _preview ?? '', style: Theme.of(context).textTheme.bodySmall, ), ), ], ); } List _actions(AppLocalizations l) { if (_busy) { return const [ Padding( padding: EdgeInsets.all(12), child: SizedBox( width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2), ), ), ]; } if (_applied != null) { return [ FilledButton( onPressed: () => Navigator.of(context).pop(), child: Text(l.guidedSetupClose), ), ]; } if (_step == 0) { return [ TextButton( onPressed: () => Navigator.of(context).pop(), child: Text(l.guidedSetupCancel), ), FilledButton(onPressed: _goReview, child: Text(l.guidedSetupNext)), ]; } return [ TextButton( onPressed: () => setState(() => _step = 0), child: Text(l.guidedSetupBack), ), FilledButton(onPressed: _apply, child: Text(l.guidedSetupApply)), ]; } }