feat(studio): guided-setup wizard (reuses chain init engine)
A Welcome 'Setup assistant' button opens a wizard that collects scenario / intent / target (+ approval & data-local toggles), then calls `chain init --answers` to preview the assembled plan and `--apply --force` to write the config — reusing the Rust deterministic engine, no logic duplicated. New SystemActions.chainInit; copyable errors via showFaiProcessError; EN+DE l10n. analyze clean; smoke test + existing welcome/sidebar tests pass. Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
parent
36a8252f67
commit
c9fa068991
9 changed files with 509 additions and 0 deletions
|
|
@ -143,6 +143,15 @@ class SystemActions {
|
||||||
return _runFai(['daemon', ...args]);
|
return _runFai(['daemon', ...args]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Run a `chain init ...` subcommand. Used by the guided-setup
|
||||||
|
/// wizard to preview a plan (`--answers <file>`) and apply it
|
||||||
|
/// (`--answers <file> --apply --force`).
|
||||||
|
static Future<({bool ok, String stdout, String stderr})> chainInit(
|
||||||
|
List<String> args,
|
||||||
|
) async {
|
||||||
|
return _runFai(['init', ...args]);
|
||||||
|
}
|
||||||
|
|
||||||
/// Run `chain update apply --channel <c>`. Long-running on a slow
|
/// Run `chain update apply --channel <c>`. Long-running on a slow
|
||||||
/// network — caller should show a spinner.
|
/// network — caller should show a spinner.
|
||||||
static Future<({bool ok, String stdout, String stderr})> chainUpdateApply(
|
static Future<({bool ok, String stdout, String stderr})> chainUpdateApply(
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,19 @@
|
||||||
{
|
{
|
||||||
"@@locale": "de",
|
"@@locale": "de",
|
||||||
|
"guidedSetupTitle": "Setup-Assistent",
|
||||||
|
"guidedSetupIntro": "Beantworte drei Fragen — Ch∆In stellt passende Konfiguration, Modul-Set und Start-Flow zusammen.",
|
||||||
|
"guidedSetupScenario": "Szenario",
|
||||||
|
"guidedSetupIntent": "Aufgabe",
|
||||||
|
"guidedSetupTarget": "Wo läuft es?",
|
||||||
|
"guidedSetupApproval": "Menschlichen Freigabe-Schritt vor dem KI-Schritt verlangen",
|
||||||
|
"guidedSetupDataLocal": "Daten müssen lokal bleiben (air-gapped)",
|
||||||
|
"guidedSetupReviewHeading": "Diesen Plan wird Ch∆In anwenden:",
|
||||||
|
"guidedSetupApply": "Setup übernehmen",
|
||||||
|
"guidedSetupApplied": "Setup übernommen. Schließe mit den Schritten unten ab:",
|
||||||
|
"guidedSetupBack": "Zurück",
|
||||||
|
"guidedSetupNext": "Weiter",
|
||||||
|
"guidedSetupCancel": "Abbrechen",
|
||||||
|
"guidedSetupClose": "Schließen",
|
||||||
"appTitle": "Ch∆In Studio",
|
"appTitle": "Ch∆In Studio",
|
||||||
"navWelcome": "Willkommen",
|
"navWelcome": "Willkommen",
|
||||||
"welcomeHeroTitle": "Ch∆In Platform",
|
"welcomeHeroTitle": "Ch∆In Platform",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,19 @@
|
||||||
{
|
{
|
||||||
"@@locale": "en",
|
"@@locale": "en",
|
||||||
|
"guidedSetupTitle": "Setup assistant",
|
||||||
|
"guidedSetupIntro": "Answer three questions and Ch∆In assembles a tailored config, module set and starter flow.",
|
||||||
|
"guidedSetupScenario": "Scenario",
|
||||||
|
"guidedSetupIntent": "Task",
|
||||||
|
"guidedSetupTarget": "Where will it run?",
|
||||||
|
"guidedSetupApproval": "Require a human approval step before the AI step",
|
||||||
|
"guidedSetupDataLocal": "Data must stay local (air-gapped posture)",
|
||||||
|
"guidedSetupReviewHeading": "This is the plan Ch∆In will apply:",
|
||||||
|
"guidedSetupApply": "Apply setup",
|
||||||
|
"guidedSetupApplied": "Setup applied. Finish with the steps below:",
|
||||||
|
"guidedSetupBack": "Back",
|
||||||
|
"guidedSetupNext": "Next",
|
||||||
|
"guidedSetupCancel": "Cancel",
|
||||||
|
"guidedSetupClose": "Close",
|
||||||
"appTitle": "Ch∆In Studio",
|
"appTitle": "Ch∆In Studio",
|
||||||
"@appTitle": {
|
"@appTitle": {
|
||||||
"description": "App-bar title in the OS task switcher."
|
"description": "App-bar title in the OS task switcher."
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,90 @@ abstract class AppLocalizations {
|
||||||
Locale('en'),
|
Locale('en'),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/// No description provided for @guidedSetupTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Setup assistant'**
|
||||||
|
String get guidedSetupTitle;
|
||||||
|
|
||||||
|
/// No description provided for @guidedSetupIntro.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Answer three questions and Ch∆In assembles a tailored config, module set and starter flow.'**
|
||||||
|
String get guidedSetupIntro;
|
||||||
|
|
||||||
|
/// No description provided for @guidedSetupScenario.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Scenario'**
|
||||||
|
String get guidedSetupScenario;
|
||||||
|
|
||||||
|
/// No description provided for @guidedSetupIntent.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Task'**
|
||||||
|
String get guidedSetupIntent;
|
||||||
|
|
||||||
|
/// No description provided for @guidedSetupTarget.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Where will it run?'**
|
||||||
|
String get guidedSetupTarget;
|
||||||
|
|
||||||
|
/// No description provided for @guidedSetupApproval.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Require a human approval step before the AI step'**
|
||||||
|
String get guidedSetupApproval;
|
||||||
|
|
||||||
|
/// No description provided for @guidedSetupDataLocal.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Data must stay local (air-gapped posture)'**
|
||||||
|
String get guidedSetupDataLocal;
|
||||||
|
|
||||||
|
/// No description provided for @guidedSetupReviewHeading.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'This is the plan Ch∆In will apply:'**
|
||||||
|
String get guidedSetupReviewHeading;
|
||||||
|
|
||||||
|
/// No description provided for @guidedSetupApply.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Apply setup'**
|
||||||
|
String get guidedSetupApply;
|
||||||
|
|
||||||
|
/// No description provided for @guidedSetupApplied.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Setup applied. Finish with the steps below:'**
|
||||||
|
String get guidedSetupApplied;
|
||||||
|
|
||||||
|
/// No description provided for @guidedSetupBack.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Back'**
|
||||||
|
String get guidedSetupBack;
|
||||||
|
|
||||||
|
/// No description provided for @guidedSetupNext.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Next'**
|
||||||
|
String get guidedSetupNext;
|
||||||
|
|
||||||
|
/// No description provided for @guidedSetupCancel.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Cancel'**
|
||||||
|
String get guidedSetupCancel;
|
||||||
|
|
||||||
|
/// No description provided for @guidedSetupClose.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Close'**
|
||||||
|
String get guidedSetupClose;
|
||||||
|
|
||||||
/// App-bar title in the OS task switcher.
|
/// App-bar title in the OS task switcher.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,51 @@ import 'app_localizations.dart';
|
||||||
class AppLocalizationsDe extends AppLocalizations {
|
class AppLocalizationsDe extends AppLocalizations {
|
||||||
AppLocalizationsDe([String locale = 'de']) : super(locale);
|
AppLocalizationsDe([String locale = 'de']) : super(locale);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guidedSetupTitle => 'Setup-Assistent';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guidedSetupIntro =>
|
||||||
|
'Beantworte drei Fragen — Ch∆In stellt passende Konfiguration, Modul-Set und Start-Flow zusammen.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guidedSetupScenario => 'Szenario';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guidedSetupIntent => 'Aufgabe';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guidedSetupTarget => 'Wo läuft es?';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guidedSetupApproval =>
|
||||||
|
'Menschlichen Freigabe-Schritt vor dem KI-Schritt verlangen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guidedSetupDataLocal => 'Daten müssen lokal bleiben (air-gapped)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guidedSetupReviewHeading => 'Diesen Plan wird Ch∆In anwenden:';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guidedSetupApply => 'Setup übernehmen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guidedSetupApplied =>
|
||||||
|
'Setup übernommen. Schließe mit den Schritten unten ab:';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guidedSetupBack => 'Zurück';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guidedSetupNext => 'Weiter';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guidedSetupCancel => 'Abbrechen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guidedSetupClose => 'Schließen';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appTitle => 'Ch∆In Studio';
|
String get appTitle => 'Ch∆In Studio';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,52 @@ import 'app_localizations.dart';
|
||||||
class AppLocalizationsEn extends AppLocalizations {
|
class AppLocalizationsEn extends AppLocalizations {
|
||||||
AppLocalizationsEn([String locale = 'en']) : super(locale);
|
AppLocalizationsEn([String locale = 'en']) : super(locale);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guidedSetupTitle => 'Setup assistant';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guidedSetupIntro =>
|
||||||
|
'Answer three questions and Ch∆In assembles a tailored config, module set and starter flow.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guidedSetupScenario => 'Scenario';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guidedSetupIntent => 'Task';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guidedSetupTarget => 'Where will it run?';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guidedSetupApproval =>
|
||||||
|
'Require a human approval step before the AI step';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guidedSetupDataLocal =>
|
||||||
|
'Data must stay local (air-gapped posture)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guidedSetupReviewHeading => 'This is the plan Ch∆In will apply:';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guidedSetupApply => 'Apply setup';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guidedSetupApplied =>
|
||||||
|
'Setup applied. Finish with the steps below:';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guidedSetupBack => 'Back';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guidedSetupNext => 'Next';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guidedSetupCancel => 'Cancel';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get guidedSetupClose => 'Close';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get appTitle => 'Ch∆In Studio';
|
String get appTitle => 'Ch∆In Studio';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import '../l10n/app_localizations.dart';
|
||||||
import '../main.dart' show StudioShellState;
|
import '../main.dart' show StudioShellState;
|
||||||
import '../theme/theme.dart';
|
import '../theme/theme.dart';
|
||||||
import '../theme/tokens.dart';
|
import '../theme/tokens.dart';
|
||||||
|
import '../widgets/guided_setup_dialog.dart';
|
||||||
import '../widgets/widgets.dart';
|
import '../widgets/widgets.dart';
|
||||||
|
|
||||||
class WelcomePage extends StatelessWidget {
|
class WelcomePage extends StatelessWidget {
|
||||||
|
|
@ -59,6 +60,15 @@ class WelcomePage extends StatelessWidget {
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const _Hero(),
|
const _Hero(),
|
||||||
|
const SizedBox(height: ChainSpace.md),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: OutlinedButton.icon(
|
||||||
|
onPressed: () => GuidedSetupDialog.show(context),
|
||||||
|
icon: const Icon(Icons.auto_fix_high),
|
||||||
|
label: Text(AppLocalizations.of(context)!.guidedSetupTitle),
|
||||||
|
),
|
||||||
|
),
|
||||||
const SizedBox(height: ChainSpace.xxl),
|
const SizedBox(height: ChainSpace.xxl),
|
||||||
// When the hub is down the onboarding checklist
|
// When the hub is down the onboarding checklist
|
||||||
// would render dead (all-unchecked, nothing to
|
// would render dead (all-unchecked, nothing to
|
||||||
|
|
|
||||||
257
lib/widgets/guided_setup_dialog.dart
Normal file
257
lib/widgets/guided_setup_dialog.dart
Normal file
|
|
@ -0,0 +1,257 @@
|
||||||
|
// 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<void> show(BuildContext context) => showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => const GuidedSetupDialog(),
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<GuidedSetupDialog> createState() => _GuidedSetupDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _GuidedSetupDialogState extends State<GuidedSetupDialog> {
|
||||||
|
// Canonical SetupAnswers enum values (serde kebab-case in Rust).
|
||||||
|
static const _scenarios = <String>[
|
||||||
|
'trying-out',
|
||||||
|
'team-hub',
|
||||||
|
'regulated-production',
|
||||||
|
'building-modules',
|
||||||
|
];
|
||||||
|
static const _intents = <String>[
|
||||||
|
'hello-world',
|
||||||
|
'extract-text',
|
||||||
|
'extract-summarize',
|
||||||
|
'classify-documents',
|
||||||
|
'build-module',
|
||||||
|
];
|
||||||
|
static const _targets = <String>[
|
||||||
|
'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<String> _writeAnswers() async {
|
||||||
|
final f = File('${Directory.systemTemp.path}/chain-setup-answers.yaml');
|
||||||
|
await f.writeAsString(_answersYaml());
|
||||||
|
return f.path;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _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<void> _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<String> items,
|
||||||
|
String value,
|
||||||
|
ValueChanged<String> onChanged,
|
||||||
|
) {
|
||||||
|
return InputDecorator(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: label,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
isDense: true,
|
||||||
|
),
|
||||||
|
child: DropdownButtonHideUnderline(
|
||||||
|
child: DropdownButton<String>(
|
||||||
|
isExpanded: true,
|
||||||
|
value: value,
|
||||||
|
items: [
|
||||||
|
for (final i in items)
|
||||||
|
DropdownMenuItem<String>(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<Widget> _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)),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
30
test/guided_setup_test.dart
Normal file
30
test/guided_setup_test.dart
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
// Smoke test for the guided-setup wizard: it mounts and renders its
|
||||||
|
// first step (three choice dropdowns) without touching the CLI. The
|
||||||
|
// `chain init` shell-out only happens on Next/Apply, so mounting is
|
||||||
|
// side-effect-free and safe to assert in a widget test.
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:chain_studio/l10n/app_localizations.dart';
|
||||||
|
import 'package:chain_studio/widgets/guided_setup_dialog.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
testWidgets('guided setup wizard mounts and shows the first step',
|
||||||
|
(tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
locale: const Locale('en'),
|
||||||
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||||
|
supportedLocales: AppLocalizations.supportedLocales,
|
||||||
|
home: const Scaffold(body: GuidedSetupDialog()),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
// Title from l10n + the three scenario/intent/target dropdowns.
|
||||||
|
expect(find.text('Setup assistant'), findsWidgets);
|
||||||
|
expect(find.byType(DropdownButton<String>), findsNWidgets(3));
|
||||||
|
// The "Next" action is present (advances to the review/apply step).
|
||||||
|
expect(find.widgetWithText(FilledButton, 'Next'), findsOneWidget);
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue