diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 9ab0d92..cefdd8d 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1695,5 +1695,59 @@ "workspaceSealedSwitchFailed": "Wechsel in den abgeschotteten Bereich fehlgeschlagen: {error}", "@workspaceSealedSwitchFailed": {"placeholders": {"error": {"type": "String"}}}, "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" } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 8bc20a7..cfb73e2 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1734,5 +1734,59 @@ "workspaceSealedSwitchFailed": "Could not switch to the sealed area: {error}", "@workspaceSealedSwitchFailed": {"placeholders": {"error": {"type": "String"}}}, "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" } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 659c3d0..e0be5a4 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -5046,6 +5046,306 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Leave'** 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 diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index afbe907..e78a64b 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -2964,4 +2964,185 @@ class AppLocalizationsDe extends AppLocalizations { @override 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'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 089a5cb..6a7d777 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -2967,4 +2967,180 @@ class AppLocalizationsEn extends AppLocalizations { @override 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'; } diff --git a/lib/widgets/guided_setup_dialog.dart b/lib/widgets/guided_setup_dialog.dart index 94f7b8c..a5dd4e6 100644 --- a/lib/widgets/guided_setup_dialog.dart +++ b/lib/widgets/guided_setup_dialog.dart @@ -1,14 +1,15 @@ // 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. +// adaptive toggles) across three explained steps, then previews the +// assembled plan in localized PLAIN LANGUAGE and applies it. All the +// policy lives in the Rust deterministic engine +// (chain_core::guided_setup); this is a thin UI over `chain init`. // -// 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. +// The preview is rendered from `chain init --answers … --plan-json` +// (a structured SetupPlan), so the operator sees a German/English +// plain-language summary — never the CLI's English prose. Every +// option carries a one-line explanation of what it configures. +import 'dart:convert'; import 'dart:io'; import 'package:flutter/material.dart'; @@ -16,58 +17,66 @@ import 'package:flutter/material.dart'; import '../data/error_presentation.dart'; import '../data/system_actions.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 { const GuidedSetupDialog({super.key}); - /// Launch the wizard as a modal dialog. static Future show(BuildContext context) => showDialog( - context: context, - builder: (_) => const GuidedSetupDialog(), - ); + 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', + // Canonical SetupAnswers wire values (serde kebab-case in Rust) with + // their localized label + explanation. + static final _scenarios = <_Option>[ + _Option('trying-out', (l) => l.setupScenTryingOut, (l) => l.setupScenTryingOutSub), + _Option('team-hub', (l) => l.setupScenTeamHub, (l) => l.setupScenTeamHubSub), + _Option('regulated-production', (l) => l.setupScenRegulated, (l) => l.setupScenRegulatedSub), + _Option('building-modules', (l) => l.setupScenBuildModules, (l) => l.setupScenBuildModulesSub), ]; - static const _intents = [ - 'hello-world', - 'extract-text', - 'extract-summarize', - 'classify-documents', - 'build-module', + static final _intents = <_Option>[ + _Option('hello-world', (l) => l.setupIntentHello, (l) => l.setupIntentHelloSub), + _Option('extract-text', (l) => l.setupIntentExtract, (l) => l.setupIntentExtractSub), + _Option('extract-summarize', (l) => l.setupIntentExtractSummarize, (l) => l.setupIntentExtractSummarizeSub), + _Option('classify-documents', (l) => l.setupIntentClassify, (l) => l.setupIntentClassifySub), + _Option('build-module', (l) => l.setupIntentBuildModule, (l) => l.setupIntentBuildModuleSub), ]; - static const _targets = [ - 'this-laptop', - 'home-server', - 'air-gapped-server', - 'container', + static final _targets = <_Option>[ + _Option('this-laptop', (l) => l.setupTargetLaptop, (l) => l.setupTargetLaptopSub), + _Option('home-server', (l) => l.setupTargetHomeServer, (l) => l.setupTargetHomeServerSub), + _Option('air-gapped-server', (l) => l.setupTargetAirgapped, (l) => l.setupTargetAirgappedSub), + _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 _intent = 'extract-summarize'; String _target = 'this-laptop'; bool _requireApproval = false; bool _dataLocal = false; bool _busy = false; - String? _preview; - String? _applied; + Map? _plan; // parsed SetupPlan from --plan-json + bool _applied = false; - String _humanize(String kebab) => kebab - .split('-') - .map((w) => w.isEmpty ? w : '${w[0].toUpperCase()}${w.substring(1)}') - .join(' '); - - String _answersYaml() => 'scenario: $_scenario\n' + String _answersYaml() => + 'scenario: $_scenario\n' 'intent: $_intent\n' 'target: $_target\n' 'require_approval: $_requireApproval\n' @@ -79,22 +88,36 @@ class _GuidedSetupDialogState extends State { 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 _goReview() async { setState(() { _busy = true; - _preview = null; + _plan = null; }); final path = await _writeAnswers(); - final r = await SystemActions.chainInit(['--answers', path]); + final r = await SystemActions.chainInit(['--answers', path, '--plan-json']); if (!mounted) return; setState(() => _busy = false); if (r.ok) { - setState(() { - _preview = r.stdout.trim(); - _step = 1; - }); + try { + final plan = jsonDecode(r.stdout.trim()) as Map; + setState(() { + _plan = plan; + _step = _totalSteps; // review + }); + } catch (_) { + showFaiProcessError(context, 'chain init --plan-json', r.stdout, r.stderr); + } } 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 { if (!mounted) return; setState(() => _busy = false); if (r.ok) { - setState(() => _applied = r.stdout.trim()); + setState(() => _applied = true); } else { showFaiProcessError(context, 'chain init --apply', r.stdout, r.stderr); } @@ -116,106 +139,167 @@ class _GuidedSetupDialogState extends State { @override Widget build(BuildContext context) { final l = AppLocalizations.of(context)!; + final reviewing = _step >= _totalSteps; return AlertDialog( - title: Text(l.guidedSetupTitle), + title: Text(reviewing ? l.setupReviewTitle : _stepTitle(l)), content: SizedBox( - width: 460, + width: 480, child: SingleChildScrollView( - child: _step == 0 ? _answersStep(l) : _reviewStep(l), + child: reviewing ? _reviewStep(l) : _answerStep(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( 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), + Padding( + padding: const EdgeInsets.only(bottom: ChainSpace.sm), + child: Text( + l.setupStepOf(_step + 1, _totalSteps), + style: Theme.of(context).textTheme.labelSmall?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), ), + for (final o in options) + _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( + contentPadding: EdgeInsets.zero, + title: Text(l.setupApprovalPlain), + value: _requireApproval, + onChanged: (v) => setState(() => _requireApproval = v), + ), + SwitchListTile( + contentPadding: EdgeInsets.zero, + title: Text(l.setupDataLocalPlain), + 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; + final plan = _plan; + if (plan == null) return const SizedBox.shrink(); + final theme = Theme.of(context); + final lines = _planLines(l, plan); 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), + if (_applied) ...[ + Row( + children: [ + Icon(Icons.check_circle, color: ChainColors.success, size: 20), + const SizedBox(width: ChainSpace.sm), + Text(l.setupApplied, style: theme.textTheme.titleSmall), + ], ), - // 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, - ), - ), + const SizedBox(height: ChainSpace.md), + 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, + ), + ), + ), + ], + ), + ], + ], ], ); } + 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 _planLines(AppLocalizations l, Map plan) { + final lines = []; + 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() ?? 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 _actions(AppLocalizations l) { if (_busy) { return const [ @@ -229,7 +313,7 @@ class _GuidedSetupDialogState extends State { ), ]; } - if (_applied != null) { + if (_applied) { return [ FilledButton( onPressed: () => Navigator.of(context).pop(), @@ -237,21 +321,111 @@ class _GuidedSetupDialogState extends State { ), ]; } - if (_step == 0) { + if (_step < _totalSteps) { return [ TextButton( - onPressed: () => Navigator.of(context).pop(), - child: Text(l.guidedSetupCancel), + onPressed: _step == 0 + ? () => 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 [ TextButton( - onPressed: () => setState(() => _step = 0), + onPressed: () => setState(() => _step = _totalSteps - 1), child: Text(l.guidedSetupBack), ), 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, + ), + ), + ], + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/test/guided_setup_test.dart b/test/guided_setup_test.dart index b6fa967..23d2836 100644 --- a/test/guided_setup_test.dart +++ b/test/guided_setup_test.dart @@ -1,30 +1,78 @@ -// 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. +// Guided-setup wizard — the three explained answer steps. Verifies +// that options render as localized cards with their one-line +// explanation (no English enum humanization) and that Weiter/Zurück +// 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_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()), +Widget _host() => MaterialApp( + locale: const Locale('de'), + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + 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. - expect(find.text('Setup assistant'), findsWidgets); - expect(find.byType(DropdownButton), findsNWidgets(3)); - // The "Next" action is present (advances to the review/apply step). - expect(find.widgetWithText(FilledButton, 'Next'), findsOneWidget); +void main() { + testWidgets('step 1 shows scenario options in German with explanations', ( + tester, + ) async { + 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); }); }