feat: guided-setup wizard — localized explained options + plain-language plan
Some checks failed
Security / Security check (push) Failing after 1s
Some checks failed
Security / Security check (push) Failing after 1s
Reworks the Setup-Assistent toward the zero-learning-curve bar
(docs/architecture/guided-setup.md, phase 1.1):
- Every scenario/intent/target choice is now a localized option CARD
with a one-line plain-language explanation of what it configures
(DE+EN, Sie-form) — replacing the bare dropdowns whose labels were
English enum humanizations ('Regulated Production', 'This Laptop').
- Three explained steps with a 'Schritt n von 3' progress line
(stakes → task → environment); the two adaptive toggles move to the
last step in plain language (no 'air-gapped' jargon).
- The review step renders a localized PLAIN-LANGUAGE summary built
from 'chain init --answers --plan-json' (the structured SetupPlan) —
'Ch∆In richtet einen regulierten Betrieb ein: signierte Module
verlangt · … · geändert wird nur ~/.chain/config.yaml' — instead of
echoing the CLI's English prose. Warns when an existing config will
be overwritten. After apply: a plain 'Fertig' + next steps.
flutter analyze clean; widget tests for the step flow + German option
labels. Remaining per plan: clickable follow-up actions, signature
dead-end fix, placement/auto-open, and the LLM free-text path.
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
parent
0073d77a67
commit
140408d26a
7 changed files with 1133 additions and 146 deletions
|
|
@ -1695,5 +1695,59 @@
|
||||||
"workspaceSealedSwitchFailed": "Wechsel in den abgeschotteten Bereich fehlgeschlagen: {error}",
|
"workspaceSealedSwitchFailed": "Wechsel in den abgeschotteten Bereich fehlgeschlagen: {error}",
|
||||||
"@workspaceSealedSwitchFailed": {"placeholders": {"error": {"type": "String"}}},
|
"@workspaceSealedSwitchFailed": {"placeholders": {"error": {"type": "String"}}},
|
||||||
"sealedIdentityBar": "Abgeschotteter Bereich — isolierter Hub, eigene Daten und Audit-Kette",
|
"sealedIdentityBar": "Abgeschotteter Bereich — isolierter Hub, eigene Daten und Audit-Kette",
|
||||||
"sealedLeave": "Verlassen"
|
"sealedLeave": "Verlassen",
|
||||||
|
"setupStepOf": "Schritt {n} von {total}",
|
||||||
|
"@setupStepOf": {"placeholders": {"n": {"type": "int"}, "total": {"type": "int"}}},
|
||||||
|
"setupStep1Title": "Worum geht es?",
|
||||||
|
"setupStep2Title": "Was möchten Sie zuerst tun?",
|
||||||
|
"setupStep3Title": "Wo läuft es?",
|
||||||
|
"setupReviewTitle": "Das wird Ch∆In einrichten",
|
||||||
|
"setupChooseFreeText": "Oder beschreiben Sie einfach, was Sie vorhaben",
|
||||||
|
"setupScenTryingOut": "Erst mal ausprobieren",
|
||||||
|
"setupScenTryingOutSub": "Ein unkomplizierter Arbeitsplatz auf diesem Rechner — ohne Signaturen, ohne Audit-Sperre.",
|
||||||
|
"setupScenTeamHub": "Ein Team-Server",
|
||||||
|
"setupScenTeamHubSub": "Ein abgesicherter Hub für mehrere Personen — signierte Module, Ressourcen-Grenzen.",
|
||||||
|
"setupScenRegulated": "Regulierter Produktivbetrieb",
|
||||||
|
"setupScenRegulatedSub": "Signierte Module, ein lückenloses, manipulationssicheres Prüfprotokoll und eine menschliche Freigabe vor jedem KI-Schritt.",
|
||||||
|
"setupScenBuildModules": "Module entwickeln",
|
||||||
|
"setupScenBuildModulesSub": "Eine Entwickler-Einrichtung zum Schreiben und Testen eigener Module.",
|
||||||
|
"setupIntentHello": "Das Hallo-Beispiel ausführen",
|
||||||
|
"setupIntentHelloSub": "Einfach einen Flow einmal von Anfang bis Ende sehen.",
|
||||||
|
"setupIntentExtract": "Text aus Dokumenten extrahieren",
|
||||||
|
"setupIntentExtractSub": "Reinen Text aus PDF- und DOCX-Dateien holen.",
|
||||||
|
"setupIntentExtractSummarize": "Extrahieren und zusammenfassen",
|
||||||
|
"setupIntentExtractSummarizeSub": "Text extrahieren und anschließend zusammenfassen.",
|
||||||
|
"setupIntentClassify": "Eingehende Dokumente einsortieren",
|
||||||
|
"setupIntentClassifySub": "Text extrahieren und mit einem KI-Schritt klassifizieren.",
|
||||||
|
"setupIntentBuildModule": "Ein eigenes Modul bauen",
|
||||||
|
"setupIntentBuildModuleSub": "Mit einem Modul-Gerüst starten.",
|
||||||
|
"setupTargetLaptop": "Dieser Rechner",
|
||||||
|
"setupTargetLaptopSub": "Läuft lokal auf dem Gerät, an dem Sie gerade sitzen.",
|
||||||
|
"setupTargetHomeServer": "Ein eigener Server",
|
||||||
|
"setupTargetHomeServerSub": "Ein Server im eigenen Haus — startet beim Hochfahren, läuft im Hintergrund.",
|
||||||
|
"setupTargetAirgapped": "Ein vollständig lokaler Server (ohne Internet)",
|
||||||
|
"setupTargetAirgappedSub": "Ein isolierter Server ohne Internet — die strengste, privateste Betriebsart.",
|
||||||
|
"setupTargetContainer": "Ein Container",
|
||||||
|
"setupTargetContainerSub": "Docker oder Podman, mit fertigem Compose-Schnipsel.",
|
||||||
|
"setupApprovalPlain": "Vor jedem KI-Schritt einen Menschen um Freigabe bitten",
|
||||||
|
"setupDataLocalPlain": "Daten dürfen das Haus nie verlassen (vollständig lokal)",
|
||||||
|
"setupPlanIntroDev": "Ch∆In richtet einen unkomplizierten Arbeitsplatz ein.",
|
||||||
|
"setupPlanIntroEnterprise": "Ch∆In richtet einen abgesicherten Betrieb für ein Team ein.",
|
||||||
|
"setupPlanIntroAirgapped": "Ch∆In richtet einen vollständig lokalen, regulierten Betrieb ein.",
|
||||||
|
"setupPlanSignatures": "Es werden nur signierte Module zugelassen.",
|
||||||
|
"setupPlanWorm": "Das Prüfprotokoll wird zusätzlich manipulationssicher (WORM) gesichert.",
|
||||||
|
"setupPlanApproval": "Vor jedem KI-Schritt fragt das System einen Menschen um Freigabe.",
|
||||||
|
"setupPlanModules": "Diese Module werden vorbereitet: {modules}.",
|
||||||
|
"@setupPlanModules": {"placeholders": {"modules": {"type": "String"}}},
|
||||||
|
"setupPlanFlow": "Ein Beispiel-Flow „{flow}“ wird angelegt.",
|
||||||
|
"@setupPlanFlow": {"placeholders": {"flow": {"type": "String"}}},
|
||||||
|
"setupPlanFileChanged": "Geändert wird nur eine Datei: {path}.",
|
||||||
|
"@setupPlanFileChanged": {"placeholders": {"path": {"type": "String"}}},
|
||||||
|
"setupPlanOverwriteWarn": "Eine vorhandene Konfiguration wird überschrieben.",
|
||||||
|
"setupApplied": "Fertig — Ch∆In ist eingerichtet.",
|
||||||
|
"setupNextTitle": "Nächste Schritte",
|
||||||
|
"setupNextHubStart": "Hub starten",
|
||||||
|
"setupNextInstall": "Die Module installieren",
|
||||||
|
"setupNextRunFlow": "Den Beispiel-Flow ausführen",
|
||||||
|
"setupStartCta": "In 3 Fragen loslegen"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1734,5 +1734,59 @@
|
||||||
"workspaceSealedSwitchFailed": "Could not switch to the sealed area: {error}",
|
"workspaceSealedSwitchFailed": "Could not switch to the sealed area: {error}",
|
||||||
"@workspaceSealedSwitchFailed": {"placeholders": {"error": {"type": "String"}}},
|
"@workspaceSealedSwitchFailed": {"placeholders": {"error": {"type": "String"}}},
|
||||||
"sealedIdentityBar": "Sealed area — isolated hub, own data and audit chain",
|
"sealedIdentityBar": "Sealed area — isolated hub, own data and audit chain",
|
||||||
"sealedLeave": "Leave"
|
"sealedLeave": "Leave",
|
||||||
|
"setupStepOf": "Step {n} of {total}",
|
||||||
|
"@setupStepOf": {"placeholders": {"n": {"type": "int"}, "total": {"type": "int"}}},
|
||||||
|
"setupStep1Title": "What are the stakes?",
|
||||||
|
"setupStep2Title": "What do you want to do first?",
|
||||||
|
"setupStep3Title": "Where will it run?",
|
||||||
|
"setupReviewTitle": "This is what Ch∆In will set up",
|
||||||
|
"setupChooseFreeText": "Or just describe what you want to do",
|
||||||
|
"setupScenTryingOut": "Just trying it out",
|
||||||
|
"setupScenTryingOutSub": "A relaxed workspace on this machine — no signing, no audit locking.",
|
||||||
|
"setupScenTeamHub": "A shared team hub",
|
||||||
|
"setupScenTeamHubSub": "A secured hub for several people — signed modules, resource limits.",
|
||||||
|
"setupScenRegulated": "Regulated production",
|
||||||
|
"setupScenRegulatedSub": "Signed modules, a complete tamper-evident audit trail, and a human approval before every AI step.",
|
||||||
|
"setupScenBuildModules": "Building modules",
|
||||||
|
"setupScenBuildModulesSub": "A developer setup for writing and testing your own modules.",
|
||||||
|
"setupIntentHello": "Run the hello example",
|
||||||
|
"setupIntentHelloSub": "Just see one flow run end to end.",
|
||||||
|
"setupIntentExtract": "Extract text from documents",
|
||||||
|
"setupIntentExtractSub": "Pull plain text out of PDF and DOCX files.",
|
||||||
|
"setupIntentExtractSummarize": "Extract and summarize",
|
||||||
|
"setupIntentExtractSummarizeSub": "Extract text, then summarize it.",
|
||||||
|
"setupIntentClassify": "Sort incoming documents",
|
||||||
|
"setupIntentClassifySub": "Extract text, then classify it with an AI step.",
|
||||||
|
"setupIntentBuildModule": "Build my own module",
|
||||||
|
"setupIntentBuildModuleSub": "Start from a module scaffold.",
|
||||||
|
"setupTargetLaptop": "This computer",
|
||||||
|
"setupTargetLaptopSub": "Runs locally on the machine you are on now.",
|
||||||
|
"setupTargetHomeServer": "An on-premise server",
|
||||||
|
"setupTargetHomeServerSub": "A server in your own house — starts on boot, runs in the background.",
|
||||||
|
"setupTargetAirgapped": "A fully local server (no internet)",
|
||||||
|
"setupTargetAirgappedSub": "An isolated server with no internet — the strictest, most private posture.",
|
||||||
|
"setupTargetContainer": "A container",
|
||||||
|
"setupTargetContainerSub": "Docker or Podman, with a ready-made compose snippet.",
|
||||||
|
"setupApprovalPlain": "Ask a person for approval before every AI step",
|
||||||
|
"setupDataLocalPlain": "Data must never leave the building (fully local)",
|
||||||
|
"setupPlanIntroDev": "Ch∆In sets up a straightforward workspace.",
|
||||||
|
"setupPlanIntroEnterprise": "Ch∆In sets up a secured operation for a team.",
|
||||||
|
"setupPlanIntroAirgapped": "Ch∆In sets up a fully local, regulated operation.",
|
||||||
|
"setupPlanSignatures": "Only signed modules are allowed.",
|
||||||
|
"setupPlanWorm": "The audit log is additionally sealed tamper-proof (WORM).",
|
||||||
|
"setupPlanApproval": "Before every AI step, the system asks a person for approval.",
|
||||||
|
"setupPlanModules": "These modules are prepared: {modules}.",
|
||||||
|
"@setupPlanModules": {"placeholders": {"modules": {"type": "String"}}},
|
||||||
|
"setupPlanFlow": "An example flow “{flow}” is created.",
|
||||||
|
"@setupPlanFlow": {"placeholders": {"flow": {"type": "String"}}},
|
||||||
|
"setupPlanFileChanged": "Only one file is changed: {path}.",
|
||||||
|
"@setupPlanFileChanged": {"placeholders": {"path": {"type": "String"}}},
|
||||||
|
"setupPlanOverwriteWarn": "An existing configuration will be overwritten.",
|
||||||
|
"setupApplied": "Done — Ch∆In is set up.",
|
||||||
|
"setupNextTitle": "Next steps",
|
||||||
|
"setupNextHubStart": "Start the hub",
|
||||||
|
"setupNextInstall": "Install the modules",
|
||||||
|
"setupNextRunFlow": "Run the example flow",
|
||||||
|
"setupStartCta": "Get started in 3 questions"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5046,6 +5046,306 @@ abstract class AppLocalizations {
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Leave'**
|
/// **'Leave'**
|
||||||
String get sealedLeave;
|
String get sealedLeave;
|
||||||
|
|
||||||
|
/// No description provided for @setupStepOf.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Step {n} of {total}'**
|
||||||
|
String setupStepOf(int n, int total);
|
||||||
|
|
||||||
|
/// No description provided for @setupStep1Title.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'What are the stakes?'**
|
||||||
|
String get setupStep1Title;
|
||||||
|
|
||||||
|
/// No description provided for @setupStep2Title.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'What do you want to do first?'**
|
||||||
|
String get setupStep2Title;
|
||||||
|
|
||||||
|
/// No description provided for @setupStep3Title.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Where will it run?'**
|
||||||
|
String get setupStep3Title;
|
||||||
|
|
||||||
|
/// No description provided for @setupReviewTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'This is what Ch∆In will set up'**
|
||||||
|
String get setupReviewTitle;
|
||||||
|
|
||||||
|
/// No description provided for @setupChooseFreeText.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Or just describe what you want to do'**
|
||||||
|
String get setupChooseFreeText;
|
||||||
|
|
||||||
|
/// No description provided for @setupScenTryingOut.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Just trying it out'**
|
||||||
|
String get setupScenTryingOut;
|
||||||
|
|
||||||
|
/// No description provided for @setupScenTryingOutSub.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'A relaxed workspace on this machine — no signing, no audit locking.'**
|
||||||
|
String get setupScenTryingOutSub;
|
||||||
|
|
||||||
|
/// No description provided for @setupScenTeamHub.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'A shared team hub'**
|
||||||
|
String get setupScenTeamHub;
|
||||||
|
|
||||||
|
/// No description provided for @setupScenTeamHubSub.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'A secured hub for several people — signed modules, resource limits.'**
|
||||||
|
String get setupScenTeamHubSub;
|
||||||
|
|
||||||
|
/// No description provided for @setupScenRegulated.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Regulated production'**
|
||||||
|
String get setupScenRegulated;
|
||||||
|
|
||||||
|
/// No description provided for @setupScenRegulatedSub.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Signed modules, a complete tamper-evident audit trail, and a human approval before every AI step.'**
|
||||||
|
String get setupScenRegulatedSub;
|
||||||
|
|
||||||
|
/// No description provided for @setupScenBuildModules.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Building modules'**
|
||||||
|
String get setupScenBuildModules;
|
||||||
|
|
||||||
|
/// No description provided for @setupScenBuildModulesSub.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'A developer setup for writing and testing your own modules.'**
|
||||||
|
String get setupScenBuildModulesSub;
|
||||||
|
|
||||||
|
/// No description provided for @setupIntentHello.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Run the hello example'**
|
||||||
|
String get setupIntentHello;
|
||||||
|
|
||||||
|
/// No description provided for @setupIntentHelloSub.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Just see one flow run end to end.'**
|
||||||
|
String get setupIntentHelloSub;
|
||||||
|
|
||||||
|
/// No description provided for @setupIntentExtract.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Extract text from documents'**
|
||||||
|
String get setupIntentExtract;
|
||||||
|
|
||||||
|
/// No description provided for @setupIntentExtractSub.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Pull plain text out of PDF and DOCX files.'**
|
||||||
|
String get setupIntentExtractSub;
|
||||||
|
|
||||||
|
/// No description provided for @setupIntentExtractSummarize.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Extract and summarize'**
|
||||||
|
String get setupIntentExtractSummarize;
|
||||||
|
|
||||||
|
/// No description provided for @setupIntentExtractSummarizeSub.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Extract text, then summarize it.'**
|
||||||
|
String get setupIntentExtractSummarizeSub;
|
||||||
|
|
||||||
|
/// No description provided for @setupIntentClassify.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Sort incoming documents'**
|
||||||
|
String get setupIntentClassify;
|
||||||
|
|
||||||
|
/// No description provided for @setupIntentClassifySub.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Extract text, then classify it with an AI step.'**
|
||||||
|
String get setupIntentClassifySub;
|
||||||
|
|
||||||
|
/// No description provided for @setupIntentBuildModule.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Build my own module'**
|
||||||
|
String get setupIntentBuildModule;
|
||||||
|
|
||||||
|
/// No description provided for @setupIntentBuildModuleSub.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Start from a module scaffold.'**
|
||||||
|
String get setupIntentBuildModuleSub;
|
||||||
|
|
||||||
|
/// No description provided for @setupTargetLaptop.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'This computer'**
|
||||||
|
String get setupTargetLaptop;
|
||||||
|
|
||||||
|
/// No description provided for @setupTargetLaptopSub.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Runs locally on the machine you are on now.'**
|
||||||
|
String get setupTargetLaptopSub;
|
||||||
|
|
||||||
|
/// No description provided for @setupTargetHomeServer.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'An on-premise server'**
|
||||||
|
String get setupTargetHomeServer;
|
||||||
|
|
||||||
|
/// No description provided for @setupTargetHomeServerSub.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'A server in your own house — starts on boot, runs in the background.'**
|
||||||
|
String get setupTargetHomeServerSub;
|
||||||
|
|
||||||
|
/// No description provided for @setupTargetAirgapped.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'A fully local server (no internet)'**
|
||||||
|
String get setupTargetAirgapped;
|
||||||
|
|
||||||
|
/// No description provided for @setupTargetAirgappedSub.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'An isolated server with no internet — the strictest, most private posture.'**
|
||||||
|
String get setupTargetAirgappedSub;
|
||||||
|
|
||||||
|
/// No description provided for @setupTargetContainer.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'A container'**
|
||||||
|
String get setupTargetContainer;
|
||||||
|
|
||||||
|
/// No description provided for @setupTargetContainerSub.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Docker or Podman, with a ready-made compose snippet.'**
|
||||||
|
String get setupTargetContainerSub;
|
||||||
|
|
||||||
|
/// No description provided for @setupApprovalPlain.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Ask a person for approval before every AI step'**
|
||||||
|
String get setupApprovalPlain;
|
||||||
|
|
||||||
|
/// No description provided for @setupDataLocalPlain.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Data must never leave the building (fully local)'**
|
||||||
|
String get setupDataLocalPlain;
|
||||||
|
|
||||||
|
/// No description provided for @setupPlanIntroDev.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Ch∆In sets up a straightforward workspace.'**
|
||||||
|
String get setupPlanIntroDev;
|
||||||
|
|
||||||
|
/// No description provided for @setupPlanIntroEnterprise.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Ch∆In sets up a secured operation for a team.'**
|
||||||
|
String get setupPlanIntroEnterprise;
|
||||||
|
|
||||||
|
/// No description provided for @setupPlanIntroAirgapped.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Ch∆In sets up a fully local, regulated operation.'**
|
||||||
|
String get setupPlanIntroAirgapped;
|
||||||
|
|
||||||
|
/// No description provided for @setupPlanSignatures.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Only signed modules are allowed.'**
|
||||||
|
String get setupPlanSignatures;
|
||||||
|
|
||||||
|
/// No description provided for @setupPlanWorm.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'The audit log is additionally sealed tamper-proof (WORM).'**
|
||||||
|
String get setupPlanWorm;
|
||||||
|
|
||||||
|
/// No description provided for @setupPlanApproval.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Before every AI step, the system asks a person for approval.'**
|
||||||
|
String get setupPlanApproval;
|
||||||
|
|
||||||
|
/// No description provided for @setupPlanModules.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'These modules are prepared: {modules}.'**
|
||||||
|
String setupPlanModules(String modules);
|
||||||
|
|
||||||
|
/// No description provided for @setupPlanFlow.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'An example flow “{flow}” is created.'**
|
||||||
|
String setupPlanFlow(String flow);
|
||||||
|
|
||||||
|
/// No description provided for @setupPlanFileChanged.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Only one file is changed: {path}.'**
|
||||||
|
String setupPlanFileChanged(String path);
|
||||||
|
|
||||||
|
/// No description provided for @setupPlanOverwriteWarn.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'An existing configuration will be overwritten.'**
|
||||||
|
String get setupPlanOverwriteWarn;
|
||||||
|
|
||||||
|
/// No description provided for @setupApplied.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Done — Ch∆In is set up.'**
|
||||||
|
String get setupApplied;
|
||||||
|
|
||||||
|
/// No description provided for @setupNextTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Next steps'**
|
||||||
|
String get setupNextTitle;
|
||||||
|
|
||||||
|
/// No description provided for @setupNextHubStart.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Start the hub'**
|
||||||
|
String get setupNextHubStart;
|
||||||
|
|
||||||
|
/// No description provided for @setupNextInstall.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Install the modules'**
|
||||||
|
String get setupNextInstall;
|
||||||
|
|
||||||
|
/// No description provided for @setupNextRunFlow.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Run the example flow'**
|
||||||
|
String get setupNextRunFlow;
|
||||||
|
|
||||||
|
/// No description provided for @setupStartCta.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Get started in 3 questions'**
|
||||||
|
String get setupStartCta;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AppLocalizationsDelegate
|
class _AppLocalizationsDelegate
|
||||||
|
|
|
||||||
|
|
@ -2964,4 +2964,185 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get sealedLeave => 'Verlassen';
|
String get sealedLeave => 'Verlassen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String setupStepOf(int n, int total) {
|
||||||
|
return 'Schritt $n von $total';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupStep1Title => 'Worum geht es?';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupStep2Title => 'Was möchten Sie zuerst tun?';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupStep3Title => 'Wo läuft es?';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupReviewTitle => 'Das wird Ch∆In einrichten';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupChooseFreeText =>
|
||||||
|
'Oder beschreiben Sie einfach, was Sie vorhaben';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupScenTryingOut => 'Erst mal ausprobieren';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupScenTryingOutSub =>
|
||||||
|
'Ein unkomplizierter Arbeitsplatz auf diesem Rechner — ohne Signaturen, ohne Audit-Sperre.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupScenTeamHub => 'Ein Team-Server';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupScenTeamHubSub =>
|
||||||
|
'Ein abgesicherter Hub für mehrere Personen — signierte Module, Ressourcen-Grenzen.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupScenRegulated => 'Regulierter Produktivbetrieb';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupScenRegulatedSub =>
|
||||||
|
'Signierte Module, ein lückenloses, manipulationssicheres Prüfprotokoll und eine menschliche Freigabe vor jedem KI-Schritt.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupScenBuildModules => 'Module entwickeln';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupScenBuildModulesSub =>
|
||||||
|
'Eine Entwickler-Einrichtung zum Schreiben und Testen eigener Module.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupIntentHello => 'Das Hallo-Beispiel ausführen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupIntentHelloSub =>
|
||||||
|
'Einfach einen Flow einmal von Anfang bis Ende sehen.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupIntentExtract => 'Text aus Dokumenten extrahieren';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupIntentExtractSub =>
|
||||||
|
'Reinen Text aus PDF- und DOCX-Dateien holen.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupIntentExtractSummarize => 'Extrahieren und zusammenfassen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupIntentExtractSummarizeSub =>
|
||||||
|
'Text extrahieren und anschließend zusammenfassen.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupIntentClassify => 'Eingehende Dokumente einsortieren';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupIntentClassifySub =>
|
||||||
|
'Text extrahieren und mit einem KI-Schritt klassifizieren.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupIntentBuildModule => 'Ein eigenes Modul bauen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupIntentBuildModuleSub => 'Mit einem Modul-Gerüst starten.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupTargetLaptop => 'Dieser Rechner';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupTargetLaptopSub =>
|
||||||
|
'Läuft lokal auf dem Gerät, an dem Sie gerade sitzen.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupTargetHomeServer => 'Ein eigener Server';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupTargetHomeServerSub =>
|
||||||
|
'Ein Server im eigenen Haus — startet beim Hochfahren, läuft im Hintergrund.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupTargetAirgapped =>
|
||||||
|
'Ein vollständig lokaler Server (ohne Internet)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupTargetAirgappedSub =>
|
||||||
|
'Ein isolierter Server ohne Internet — die strengste, privateste Betriebsart.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupTargetContainer => 'Ein Container';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupTargetContainerSub =>
|
||||||
|
'Docker oder Podman, mit fertigem Compose-Schnipsel.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupApprovalPlain =>
|
||||||
|
'Vor jedem KI-Schritt einen Menschen um Freigabe bitten';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupDataLocalPlain =>
|
||||||
|
'Daten dürfen das Haus nie verlassen (vollständig lokal)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupPlanIntroDev =>
|
||||||
|
'Ch∆In richtet einen unkomplizierten Arbeitsplatz ein.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupPlanIntroEnterprise =>
|
||||||
|
'Ch∆In richtet einen abgesicherten Betrieb für ein Team ein.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupPlanIntroAirgapped =>
|
||||||
|
'Ch∆In richtet einen vollständig lokalen, regulierten Betrieb ein.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupPlanSignatures =>
|
||||||
|
'Es werden nur signierte Module zugelassen.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupPlanWorm =>
|
||||||
|
'Das Prüfprotokoll wird zusätzlich manipulationssicher (WORM) gesichert.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupPlanApproval =>
|
||||||
|
'Vor jedem KI-Schritt fragt das System einen Menschen um Freigabe.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String setupPlanModules(String modules) {
|
||||||
|
return 'Diese Module werden vorbereitet: $modules.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String setupPlanFlow(String flow) {
|
||||||
|
return 'Ein Beispiel-Flow „$flow“ wird angelegt.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String setupPlanFileChanged(String path) {
|
||||||
|
return 'Geändert wird nur eine Datei: $path.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupPlanOverwriteWarn =>
|
||||||
|
'Eine vorhandene Konfiguration wird überschrieben.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupApplied => 'Fertig — Ch∆In ist eingerichtet.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupNextTitle => 'Nächste Schritte';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupNextHubStart => 'Hub starten';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupNextInstall => 'Die Module installieren';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupNextRunFlow => 'Den Beispiel-Flow ausführen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupStartCta => 'In 3 Fragen loslegen';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2967,4 +2967,180 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get sealedLeave => 'Leave';
|
String get sealedLeave => 'Leave';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String setupStepOf(int n, int total) {
|
||||||
|
return 'Step $n of $total';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupStep1Title => 'What are the stakes?';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupStep2Title => 'What do you want to do first?';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupStep3Title => 'Where will it run?';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupReviewTitle => 'This is what Ch∆In will set up';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupChooseFreeText => 'Or just describe what you want to do';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupScenTryingOut => 'Just trying it out';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupScenTryingOutSub =>
|
||||||
|
'A relaxed workspace on this machine — no signing, no audit locking.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupScenTeamHub => 'A shared team hub';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupScenTeamHubSub =>
|
||||||
|
'A secured hub for several people — signed modules, resource limits.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupScenRegulated => 'Regulated production';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupScenRegulatedSub =>
|
||||||
|
'Signed modules, a complete tamper-evident audit trail, and a human approval before every AI step.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupScenBuildModules => 'Building modules';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupScenBuildModulesSub =>
|
||||||
|
'A developer setup for writing and testing your own modules.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupIntentHello => 'Run the hello example';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupIntentHelloSub => 'Just see one flow run end to end.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupIntentExtract => 'Extract text from documents';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupIntentExtractSub =>
|
||||||
|
'Pull plain text out of PDF and DOCX files.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupIntentExtractSummarize => 'Extract and summarize';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupIntentExtractSummarizeSub =>
|
||||||
|
'Extract text, then summarize it.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupIntentClassify => 'Sort incoming documents';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupIntentClassifySub =>
|
||||||
|
'Extract text, then classify it with an AI step.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupIntentBuildModule => 'Build my own module';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupIntentBuildModuleSub => 'Start from a module scaffold.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupTargetLaptop => 'This computer';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupTargetLaptopSub =>
|
||||||
|
'Runs locally on the machine you are on now.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupTargetHomeServer => 'An on-premise server';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupTargetHomeServerSub =>
|
||||||
|
'A server in your own house — starts on boot, runs in the background.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupTargetAirgapped => 'A fully local server (no internet)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupTargetAirgappedSub =>
|
||||||
|
'An isolated server with no internet — the strictest, most private posture.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupTargetContainer => 'A container';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupTargetContainerSub =>
|
||||||
|
'Docker or Podman, with a ready-made compose snippet.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupApprovalPlain =>
|
||||||
|
'Ask a person for approval before every AI step';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupDataLocalPlain =>
|
||||||
|
'Data must never leave the building (fully local)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupPlanIntroDev => 'Ch∆In sets up a straightforward workspace.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupPlanIntroEnterprise =>
|
||||||
|
'Ch∆In sets up a secured operation for a team.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupPlanIntroAirgapped =>
|
||||||
|
'Ch∆In sets up a fully local, regulated operation.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupPlanSignatures => 'Only signed modules are allowed.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupPlanWorm =>
|
||||||
|
'The audit log is additionally sealed tamper-proof (WORM).';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupPlanApproval =>
|
||||||
|
'Before every AI step, the system asks a person for approval.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String setupPlanModules(String modules) {
|
||||||
|
return 'These modules are prepared: $modules.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String setupPlanFlow(String flow) {
|
||||||
|
return 'An example flow “$flow” is created.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String setupPlanFileChanged(String path) {
|
||||||
|
return 'Only one file is changed: $path.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupPlanOverwriteWarn =>
|
||||||
|
'An existing configuration will be overwritten.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupApplied => 'Done — Ch∆In is set up.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupNextTitle => 'Next steps';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupNextHubStart => 'Start the hub';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupNextInstall => 'Install the modules';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupNextRunFlow => 'Run the example flow';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setupStartCta => 'Get started in 3 questions';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,15 @@
|
||||||
// Guided-setup wizard. Collects scenario / intent / target (plus two
|
// Guided-setup wizard. Collects scenario / intent / target (plus two
|
||||||
// adaptive toggles), then calls `chain init --answers` to PREVIEW the
|
// adaptive toggles) across three explained steps, then previews the
|
||||||
// assembled plan and `--apply` to write the config. All the policy lives
|
// assembled plan in localized PLAIN LANGUAGE and applies it. All the
|
||||||
// in the Rust deterministic engine (chain_core::guided_setup) — this is
|
// policy lives in the Rust deterministic engine
|
||||||
// a thin UI over the CLI, so the logic never diverges.
|
// (chain_core::guided_setup); this is a thin UI over `chain init`.
|
||||||
//
|
//
|
||||||
// NOTE: the dropdown option labels are humanised from the canonical
|
// The preview is rendered from `chain init --answers … --plan-json`
|
||||||
// kebab-case enum values (trying-out → "Trying Out"); localising those
|
// (a structured SetupPlan), so the operator sees a German/English
|
||||||
// per option is a follow-up. The structural UI + the applied plan
|
// plain-language summary — never the CLI's English prose. Every
|
||||||
// (rendered from the CLI's localisable output) are the meaningful parts.
|
// option carries a one-line explanation of what it configures.
|
||||||
|
|
||||||
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
@ -16,11 +17,20 @@ import 'package:flutter/material.dart';
|
||||||
import '../data/error_presentation.dart';
|
import '../data/error_presentation.dart';
|
||||||
import '../data/system_actions.dart';
|
import '../data/system_actions.dart';
|
||||||
import '../l10n/app_localizations.dart';
|
import '../l10n/app_localizations.dart';
|
||||||
|
import '../theme/tokens.dart';
|
||||||
|
|
||||||
|
/// A selectable setup option: the stable kebab wire value plus the
|
||||||
|
/// localized label + one-line explanation resolved at build time.
|
||||||
|
class _Option {
|
||||||
|
final String value;
|
||||||
|
final String Function(AppLocalizations) label;
|
||||||
|
final String Function(AppLocalizations) sub;
|
||||||
|
const _Option(this.value, this.label, this.sub);
|
||||||
|
}
|
||||||
|
|
||||||
class GuidedSetupDialog extends StatefulWidget {
|
class GuidedSetupDialog extends StatefulWidget {
|
||||||
const GuidedSetupDialog({super.key});
|
const GuidedSetupDialog({super.key});
|
||||||
|
|
||||||
/// Launch the wizard as a modal dialog.
|
|
||||||
static Future<void> show(BuildContext context) => showDialog<void>(
|
static Future<void> show(BuildContext context) => showDialog<void>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (_) => const GuidedSetupDialog(),
|
builder: (_) => const GuidedSetupDialog(),
|
||||||
|
|
@ -31,43 +41,42 @@ class GuidedSetupDialog extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _GuidedSetupDialogState extends State<GuidedSetupDialog> {
|
class _GuidedSetupDialogState extends State<GuidedSetupDialog> {
|
||||||
// Canonical SetupAnswers enum values (serde kebab-case in Rust).
|
// Canonical SetupAnswers wire values (serde kebab-case in Rust) with
|
||||||
static const _scenarios = <String>[
|
// their localized label + explanation.
|
||||||
'trying-out',
|
static final _scenarios = <_Option>[
|
||||||
'team-hub',
|
_Option('trying-out', (l) => l.setupScenTryingOut, (l) => l.setupScenTryingOutSub),
|
||||||
'regulated-production',
|
_Option('team-hub', (l) => l.setupScenTeamHub, (l) => l.setupScenTeamHubSub),
|
||||||
'building-modules',
|
_Option('regulated-production', (l) => l.setupScenRegulated, (l) => l.setupScenRegulatedSub),
|
||||||
|
_Option('building-modules', (l) => l.setupScenBuildModules, (l) => l.setupScenBuildModulesSub),
|
||||||
];
|
];
|
||||||
static const _intents = <String>[
|
static final _intents = <_Option>[
|
||||||
'hello-world',
|
_Option('hello-world', (l) => l.setupIntentHello, (l) => l.setupIntentHelloSub),
|
||||||
'extract-text',
|
_Option('extract-text', (l) => l.setupIntentExtract, (l) => l.setupIntentExtractSub),
|
||||||
'extract-summarize',
|
_Option('extract-summarize', (l) => l.setupIntentExtractSummarize, (l) => l.setupIntentExtractSummarizeSub),
|
||||||
'classify-documents',
|
_Option('classify-documents', (l) => l.setupIntentClassify, (l) => l.setupIntentClassifySub),
|
||||||
'build-module',
|
_Option('build-module', (l) => l.setupIntentBuildModule, (l) => l.setupIntentBuildModuleSub),
|
||||||
];
|
];
|
||||||
static const _targets = <String>[
|
static final _targets = <_Option>[
|
||||||
'this-laptop',
|
_Option('this-laptop', (l) => l.setupTargetLaptop, (l) => l.setupTargetLaptopSub),
|
||||||
'home-server',
|
_Option('home-server', (l) => l.setupTargetHomeServer, (l) => l.setupTargetHomeServerSub),
|
||||||
'air-gapped-server',
|
_Option('air-gapped-server', (l) => l.setupTargetAirgapped, (l) => l.setupTargetAirgappedSub),
|
||||||
'container',
|
_Option('container', (l) => l.setupTargetContainer, (l) => l.setupTargetContainerSub),
|
||||||
];
|
];
|
||||||
|
|
||||||
int _step = 0; // 0 = answers, 1 = review/apply
|
static const _totalSteps = 3; // three answer steps; the review is step 4
|
||||||
|
|
||||||
|
int _step = 0; // 0..2 answers, 3 = review/apply
|
||||||
String _scenario = 'trying-out';
|
String _scenario = 'trying-out';
|
||||||
String _intent = 'extract-summarize';
|
String _intent = 'extract-summarize';
|
||||||
String _target = 'this-laptop';
|
String _target = 'this-laptop';
|
||||||
bool _requireApproval = false;
|
bool _requireApproval = false;
|
||||||
bool _dataLocal = false;
|
bool _dataLocal = false;
|
||||||
bool _busy = false;
|
bool _busy = false;
|
||||||
String? _preview;
|
Map<String, dynamic>? _plan; // parsed SetupPlan from --plan-json
|
||||||
String? _applied;
|
bool _applied = false;
|
||||||
|
|
||||||
String _humanize(String kebab) => kebab
|
String _answersYaml() =>
|
||||||
.split('-')
|
'scenario: $_scenario\n'
|
||||||
.map((w) => w.isEmpty ? w : '${w[0].toUpperCase()}${w.substring(1)}')
|
|
||||||
.join(' ');
|
|
||||||
|
|
||||||
String _answersYaml() => 'scenario: $_scenario\n'
|
|
||||||
'intent: $_intent\n'
|
'intent: $_intent\n'
|
||||||
'target: $_target\n'
|
'target: $_target\n'
|
||||||
'require_approval: $_requireApproval\n'
|
'require_approval: $_requireApproval\n'
|
||||||
|
|
@ -79,22 +88,36 @@ class _GuidedSetupDialogState extends State<GuidedSetupDialog> {
|
||||||
return f.path;
|
return f.path;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// True when a config file already exists (so applying overwrites it).
|
||||||
|
bool _configExists() {
|
||||||
|
final home =
|
||||||
|
Platform.environment['HOME'] ?? Platform.environment['USERPROFILE'];
|
||||||
|
if (home == null) return false;
|
||||||
|
final sep = Platform.pathSeparator;
|
||||||
|
return File('$home$sep.chain${sep}config.yaml').existsSync();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _goReview() async {
|
Future<void> _goReview() async {
|
||||||
setState(() {
|
setState(() {
|
||||||
_busy = true;
|
_busy = true;
|
||||||
_preview = null;
|
_plan = null;
|
||||||
});
|
});
|
||||||
final path = await _writeAnswers();
|
final path = await _writeAnswers();
|
||||||
final r = await SystemActions.chainInit(['--answers', path]);
|
final r = await SystemActions.chainInit(['--answers', path, '--plan-json']);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() => _busy = false);
|
setState(() => _busy = false);
|
||||||
if (r.ok) {
|
if (r.ok) {
|
||||||
|
try {
|
||||||
|
final plan = jsonDecode(r.stdout.trim()) as Map<String, dynamic>;
|
||||||
setState(() {
|
setState(() {
|
||||||
_preview = r.stdout.trim();
|
_plan = plan;
|
||||||
_step = 1;
|
_step = _totalSteps; // review
|
||||||
});
|
});
|
||||||
|
} catch (_) {
|
||||||
|
showFaiProcessError(context, 'chain init --plan-json', r.stdout, r.stderr);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
showFaiProcessError(context, 'chain init --answers', r.stdout, r.stderr);
|
showFaiProcessError(context, 'chain init --plan-json', r.stdout, r.stderr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -107,7 +130,7 @@ class _GuidedSetupDialogState extends State<GuidedSetupDialog> {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() => _busy = false);
|
setState(() => _busy = false);
|
||||||
if (r.ok) {
|
if (r.ok) {
|
||||||
setState(() => _applied = r.stdout.trim());
|
setState(() => _applied = true);
|
||||||
} else {
|
} else {
|
||||||
showFaiProcessError(context, 'chain init --apply', r.stdout, r.stderr);
|
showFaiProcessError(context, 'chain init --apply', r.stdout, r.stderr);
|
||||||
}
|
}
|
||||||
|
|
@ -116,106 +139,167 @@ class _GuidedSetupDialogState extends State<GuidedSetupDialog> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l = AppLocalizations.of(context)!;
|
final l = AppLocalizations.of(context)!;
|
||||||
|
final reviewing = _step >= _totalSteps;
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: Text(l.guidedSetupTitle),
|
title: Text(reviewing ? l.setupReviewTitle : _stepTitle(l)),
|
||||||
content: SizedBox(
|
content: SizedBox(
|
||||||
width: 460,
|
width: 480,
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
child: _step == 0 ? _answersStep(l) : _reviewStep(l),
|
child: reviewing ? _reviewStep(l) : _answerStep(l),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
actions: _actions(l),
|
actions: _actions(l),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _answersStep(AppLocalizations l) {
|
String _stepTitle(AppLocalizations l) => switch (_step) {
|
||||||
|
0 => l.setupStep1Title,
|
||||||
|
1 => l.setupStep2Title,
|
||||||
|
_ => l.setupStep3Title,
|
||||||
|
};
|
||||||
|
|
||||||
|
Widget _answerStep(AppLocalizations l) {
|
||||||
|
final (options, selected, onSelect) = switch (_step) {
|
||||||
|
0 => (_scenarios, _scenario, (String v) => setState(() => _scenario = v)),
|
||||||
|
1 => (_intents, _intent, (String v) => setState(() => _intent = v)),
|
||||||
|
_ => (_targets, _target, (String v) => setState(() => _target = v)),
|
||||||
|
};
|
||||||
return Column(
|
return Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(l.guidedSetupIntro),
|
Padding(
|
||||||
const SizedBox(height: 16),
|
padding: const EdgeInsets.only(bottom: ChainSpace.sm),
|
||||||
_dropdown(l.guidedSetupScenario, _scenarios, _scenario,
|
child: Text(
|
||||||
(v) => setState(() => _scenario = v)),
|
l.setupStepOf(_step + 1, _totalSteps),
|
||||||
const SizedBox(height: 12),
|
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||||
_dropdown(l.guidedSetupIntent, _intents, _intent,
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
(v) => setState(() => _intent = v)),
|
),
|
||||||
const SizedBox(height: 12),
|
),
|
||||||
_dropdown(l.guidedSetupTarget, _targets, _target,
|
),
|
||||||
(v) => setState(() => _target = v)),
|
for (final o in options)
|
||||||
const SizedBox(height: 8),
|
_OptionCard(
|
||||||
|
title: o.label(l),
|
||||||
|
subtitle: o.sub(l),
|
||||||
|
selected: selected == o.value,
|
||||||
|
onTap: () => onSelect(o.value),
|
||||||
|
),
|
||||||
|
// The two adaptive toggles live on the last answer step, in
|
||||||
|
// plain language (no "air-gapped" jargon).
|
||||||
|
if (_step == 2) ...[
|
||||||
|
const SizedBox(height: ChainSpace.sm),
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
title: Text(l.guidedSetupApproval),
|
title: Text(l.setupApprovalPlain),
|
||||||
value: _requireApproval,
|
value: _requireApproval,
|
||||||
onChanged: (v) => setState(() => _requireApproval = v),
|
onChanged: (v) => setState(() => _requireApproval = v),
|
||||||
),
|
),
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
title: Text(l.guidedSetupDataLocal),
|
title: Text(l.setupDataLocalPlain),
|
||||||
value: _dataLocal,
|
value: _dataLocal,
|
||||||
onChanged: (v) => setState(() => _dataLocal = v),
|
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) {
|
Widget _reviewStep(AppLocalizations l) {
|
||||||
final applied = _applied;
|
final plan = _plan;
|
||||||
|
if (plan == null) return const SizedBox.shrink();
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final lines = _planLines(l, plan);
|
||||||
return Column(
|
return Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(applied != null
|
if (_applied) ...[
|
||||||
? l.guidedSetupApplied
|
Row(
|
||||||
: l.guidedSetupReviewHeading),
|
children: [
|
||||||
const SizedBox(height: 12),
|
Icon(Icons.check_circle, color: ChainColors.success, size: 20),
|
||||||
Container(
|
const SizedBox(width: ChainSpace.sm),
|
||||||
width: double.infinity,
|
Text(l.setupApplied, style: theme.textTheme.titleSmall),
|
||||||
padding: const EdgeInsets.all(12),
|
],
|
||||||
decoration: BoxDecoration(
|
),
|
||||||
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
const SizedBox(height: ChainSpace.md),
|
||||||
borderRadius: BorderRadius.circular(6),
|
Text(l.setupNextTitle, style: theme.textTheme.labelLarge),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
_next(l.setupNextHubStart),
|
||||||
|
if ((plan['modules'] as List?)?.isNotEmpty ?? false)
|
||||||
|
_next(l.setupNextInstall),
|
||||||
|
_next(l.setupNextRunFlow),
|
||||||
|
] else ...[
|
||||||
|
for (final line in lines)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 6),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text('· ', style: theme.textTheme.bodyMedium),
|
||||||
|
Expanded(
|
||||||
|
child: Text(line, style: theme.textTheme.bodyMedium),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (_configExists()) ...[
|
||||||
|
const SizedBox(height: ChainSpace.sm),
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.info_outline, size: 16, color: theme.colorScheme.tertiary),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
l.setupPlanOverwriteWarn,
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: theme.colorScheme.tertiary,
|
||||||
),
|
),
|
||||||
// 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,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _next(String label) => Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 4),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.arrow_right, size: 18, color: Theme.of(context).colorScheme.primary),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Expanded(child: Text(label, style: Theme.of(context).textTheme.bodyMedium)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Build the localized plain-language plan lines from the structured
|
||||||
|
/// SetupPlan (never the CLI prose).
|
||||||
|
List<String> _planLines(AppLocalizations l, Map<String, dynamic> plan) {
|
||||||
|
final lines = <String>[];
|
||||||
|
switch (plan['profile'] as String? ?? '') {
|
||||||
|
case 'air-gapped':
|
||||||
|
lines.add(l.setupPlanIntroAirgapped);
|
||||||
|
case 'enterprise':
|
||||||
|
lines.add(l.setupPlanIntroEnterprise);
|
||||||
|
default:
|
||||||
|
lines.add(l.setupPlanIntroDev);
|
||||||
|
}
|
||||||
|
if (plan['require_signatures'] == true) lines.add(l.setupPlanSignatures);
|
||||||
|
if (plan['worm_audit'] == true) lines.add(l.setupPlanWorm);
|
||||||
|
if (plan['approval_step'] == true) lines.add(l.setupPlanApproval);
|
||||||
|
final modules = (plan['modules'] as List?)?.cast<String>() ?? const [];
|
||||||
|
if (modules.isNotEmpty) lines.add(l.setupPlanModules(modules.join(', ')));
|
||||||
|
final flow = plan['starter_flow'] as String? ?? '';
|
||||||
|
if (flow.isNotEmpty) lines.add(l.setupPlanFlow(flow));
|
||||||
|
lines.add(l.setupPlanFileChanged('~/.chain/config.yaml'));
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
List<Widget> _actions(AppLocalizations l) {
|
List<Widget> _actions(AppLocalizations l) {
|
||||||
if (_busy) {
|
if (_busy) {
|
||||||
return const [
|
return const [
|
||||||
|
|
@ -229,7 +313,7 @@ class _GuidedSetupDialogState extends State<GuidedSetupDialog> {
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
if (_applied != null) {
|
if (_applied) {
|
||||||
return [
|
return [
|
||||||
FilledButton(
|
FilledButton(
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
|
@ -237,21 +321,111 @@ class _GuidedSetupDialogState extends State<GuidedSetupDialog> {
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
if (_step == 0) {
|
if (_step < _totalSteps) {
|
||||||
return [
|
return [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
onPressed: _step == 0
|
||||||
child: Text(l.guidedSetupCancel),
|
? () => Navigator.of(context).pop()
|
||||||
|
: () => setState(() => _step -= 1),
|
||||||
|
child: Text(_step == 0 ? l.guidedSetupCancel : l.guidedSetupBack),
|
||||||
|
),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: _step < _totalSteps - 1
|
||||||
|
? () => setState(() => _step += 1)
|
||||||
|
: _goReview,
|
||||||
|
child: Text(l.guidedSetupNext),
|
||||||
),
|
),
|
||||||
FilledButton(onPressed: _goReview, child: Text(l.guidedSetupNext)),
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
// Review step.
|
||||||
return [
|
return [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => setState(() => _step = 0),
|
onPressed: () => setState(() => _step = _totalSteps - 1),
|
||||||
child: Text(l.guidedSetupBack),
|
child: Text(l.guidedSetupBack),
|
||||||
),
|
),
|
||||||
FilledButton(onPressed: _apply, child: Text(l.guidedSetupApply)),
|
FilledButton(onPressed: _apply, child: Text(l.guidedSetupApply)),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A selectable card: title + one-line explanation, highlighted when
|
||||||
|
/// selected. Replaces the bare dropdowns so every choice explains
|
||||||
|
/// itself in place (zero-learning-curve).
|
||||||
|
class _OptionCard extends StatelessWidget {
|
||||||
|
final String title;
|
||||||
|
final String subtitle;
|
||||||
|
final bool selected;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
const _OptionCard({
|
||||||
|
required this.title,
|
||||||
|
required this.subtitle,
|
||||||
|
required this.selected,
|
||||||
|
required this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final accent = theme.colorScheme.primary;
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: ChainSpace.sm),
|
||||||
|
child: Material(
|
||||||
|
color: selected
|
||||||
|
? accent.withValues(alpha: 0.10)
|
||||||
|
: theme.colorScheme.surfaceContainerHighest,
|
||||||
|
borderRadius: BorderRadius.circular(ChainRadius.md),
|
||||||
|
child: InkWell(
|
||||||
|
borderRadius: BorderRadius.circular(ChainRadius.md),
|
||||||
|
onTap: onTap,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: ChainSpace.md,
|
||||||
|
vertical: ChainSpace.sm,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(ChainRadius.md),
|
||||||
|
border: Border.all(
|
||||||
|
color: selected ? accent : theme.colorScheme.outlineVariant,
|
||||||
|
width: selected ? 1.5 : 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
selected
|
||||||
|
? Icons.radio_button_checked
|
||||||
|
: Icons.radio_button_unchecked,
|
||||||
|
size: 18,
|
||||||
|
color: selected ? accent : theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
const SizedBox(width: ChainSpace.sm),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: theme.textTheme.titleSmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text(
|
||||||
|
subtitle,
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,78 @@
|
||||||
// Smoke test for the guided-setup wizard: it mounts and renders its
|
// Guided-setup wizard — the three explained answer steps. Verifies
|
||||||
// first step (three choice dropdowns) without touching the CLI. The
|
// that options render as localized cards with their one-line
|
||||||
// `chain init` shell-out only happens on Next/Apply, so mounting is
|
// explanation (no English enum humanization) and that Weiter/Zurück
|
||||||
// side-effect-free and safe to assert in a widget test.
|
// walk the steps. The review step calls the CLI (a subprocess) and is
|
||||||
|
// out of scope for a widget test.
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
import 'package:chain_studio/l10n/app_localizations.dart';
|
import 'package:chain_studio/l10n/app_localizations.dart';
|
||||||
import 'package:chain_studio/widgets/guided_setup_dialog.dart';
|
import 'package:chain_studio/widgets/guided_setup_dialog.dart';
|
||||||
|
|
||||||
void main() {
|
Widget _host() => MaterialApp(
|
||||||
testWidgets('guided setup wizard mounts and shows the first step',
|
locale: const Locale('de'),
|
||||||
(tester) async {
|
|
||||||
await tester.pumpWidget(
|
|
||||||
MaterialApp(
|
|
||||||
locale: const Locale('en'),
|
|
||||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||||
supportedLocales: AppLocalizations.supportedLocales,
|
supportedLocales: AppLocalizations.supportedLocales,
|
||||||
home: const Scaffold(body: GuidedSetupDialog()),
|
home: Builder(
|
||||||
|
builder: (context) => Scaffold(
|
||||||
|
body: Center(
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: () => GuidedSetupDialog.show(context),
|
||||||
|
child: const Text('open'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
await tester.pump();
|
|
||||||
|
|
||||||
// Title from l10n + the three scenario/intent/target dropdowns.
|
void main() {
|
||||||
expect(find.text('Setup assistant'), findsWidgets);
|
testWidgets('step 1 shows scenario options in German with explanations', (
|
||||||
expect(find.byType(DropdownButton<String>), findsNWidgets(3));
|
tester,
|
||||||
// The "Next" action is present (advances to the review/apply step).
|
) async {
|
||||||
expect(find.widgetWithText(FilledButton, 'Next'), findsOneWidget);
|
await tester.pumpWidget(_host());
|
||||||
|
await tester.tap(find.text('open'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Step counter + localized (German) scenario labels, not the
|
||||||
|
// English enum humanization.
|
||||||
|
expect(find.text('Schritt 1 von 3'), findsOneWidget);
|
||||||
|
expect(find.text('Regulierter Produktivbetrieb'), findsOneWidget);
|
||||||
|
expect(find.text('Erst mal ausprobieren'), findsOneWidget);
|
||||||
|
// Each option carries its one-line explanation.
|
||||||
|
expect(
|
||||||
|
find.textContaining('menschliche Freigabe vor jedem KI-Schritt'),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
// No leaked English enum labels.
|
||||||
|
expect(find.text('Regulated Production'), findsNothing);
|
||||||
|
expect(find.text('Trying Out'), findsNothing);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Weiter/Zurück walk the three answer steps', (tester) async {
|
||||||
|
await tester.pumpWidget(_host());
|
||||||
|
await tester.tap(find.text('open'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Step 1 → 2 (task).
|
||||||
|
await tester.tap(find.text('Weiter'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('Schritt 2 von 3'), findsOneWidget);
|
||||||
|
expect(find.text('Text aus Dokumenten extrahieren'), findsOneWidget);
|
||||||
|
|
||||||
|
// Step 2 → 3 (environment) shows the two plain-language toggles.
|
||||||
|
await tester.tap(find.text('Weiter'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('Schritt 3 von 3'), findsOneWidget);
|
||||||
|
expect(find.text('Dieser Rechner'), findsOneWidget);
|
||||||
|
expect(
|
||||||
|
find.text('Vor jedem KI-Schritt einen Menschen um Freigabe bitten'),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Zurück returns to step 2.
|
||||||
|
await tester.tap(find.text('Zurück'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('Schritt 2 von 3'), findsOneWidget);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue