From 930e2466499a2db6ec7659e82762298a079c1b91 Mon Sep 17 00:00:00 2001 From: flemming-it Date: Sat, 9 May 2026 00:28:41 +0200 Subject: [PATCH] =?UTF-8?q?feat(studio):=20Welcome=20page=20Phase=20C=20?= =?UTF-8?q?=E2=80=94=20live=20getting-started=20checklist=20(v0.39.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Final slice of the Welcome surface. Four steps that prove the operator has the basic loop wired up; each step is a live probe against the running hub, no manual ticks. The four checked signals: 1. System AI configured → `HubService.systemAiStatus().enabled` 2. Public capability source added → `listMcpClients()` returns at least one entry 3. Text module installed → `listModules()` contains a name starting with `text.` 4. Saved flow run → any `flow.completed` event in the last 100 audit events All four probes fire in parallel from `_OnboardingChecklist`'s `_refresh()`. Failures stay false (catchError) so a hub that is briefly unreachable doesn't blank the whole row — the operator hits the explicit Refresh icon to retry. UX: - Renders between hero and pillars so the actionable path beats the educational content for screen real estate. - Each row: status icon (○ pending / ✓ done) + title + one-line hint pointing the operator at the right Studio surface + status pill. - Done rows strike-through the title and dim it; pending rows stay full-strength. - When all four flip to done, an "All four steps complete" footer appears with a `Hide checklist` button. Clicking persists `welcome.checklist.dismissed = true` via SharedPreferences; the section is gone for good (we don't re-nag operators who chose a non-default path). 15 new ARB keys for the section header, body, four rows plus done/pending pills, all-done footer, dismiss / refresh / refreshing button labels. Three-phase plan from `docs/landing-page-design.md` is now fully shipped: Phase A scaffolding (v0.37.0), Phase B embedded doc reader (v0.38.0), Phase C live checklist (v0.39.0). Signed-off-by: flemming-it --- lib/l10n/app_de.arb | 16 ++ lib/l10n/app_en.arb | 16 ++ lib/l10n/app_localizations.dart | 96 +++++++++++ lib/l10n/app_localizations_de.dart | 53 ++++++ lib/l10n/app_localizations_en.dart | 53 ++++++ lib/main.dart | 2 +- lib/pages/welcome.dart | 260 +++++++++++++++++++++++++++++ pubspec.yaml | 2 +- 8 files changed, 496 insertions(+), 2 deletions(-) diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 7278a2b..3b955ba 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -33,6 +33,22 @@ "welcomeDocClose": "Schließen", "welcomeDocFailedToLoad": "Doku konnte nicht geladen werden: {error}", "@welcomeDocFailedToLoad": { "placeholders": { "error": { "type": "String" } } }, + "welcomeChecklistHeader": "ERSTE SCHRITTE", + "welcomeChecklistBody": "Vier Schritte zu einem funktionierenden Hub. Live-Zustand — nach jeder Änderung aktualisieren.", + "welcomeChecklistAi": "System-AI konfigurieren", + "welcomeChecklistAiHint": "Einstellungen → System-AI. Wird für die KI-Suche und Fehlererklärungen gebraucht.", + "welcomeChecklistMcp": "Öffentliche Capability-Quelle hinzufügen", + "welcomeChecklistMcpHint": "Store → Today-Hero, oder Einstellungen → MCP-Clients. DeepWiki und Semgrep sind je ein Klick.", + "welcomeChecklistModule": "Ein Text-Modul installieren", + "welcomeChecklistModuleHint": "Store → nach „text\" suchen → Installieren. text.extract für PDFs und DOCX ist ein guter Start.", + "welcomeChecklistFlow": "Einen gespeicherten Flow starten", + "welcomeChecklistFlowHint": "Flows → einen wählen → Starten. Der mitgelieferte extract-summarize.yaml ist ein guter erster Lauf.", + "welcomeChecklistAllDone": "Alle vier Schritte erledigt.", + "welcomeChecklistDismiss": "Checkliste ausblenden", + "welcomeChecklistRefresh": "Aktualisieren", + "welcomeChecklistRefreshing": "Prüfe…", + "welcomeChecklistDone": "Erledigt", + "welcomeChecklistTodo": "Offen", "navDoctor": "Diagnose", "navModules": "Module", "navStore": "Store", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 02bd993..6b9ab19 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -34,6 +34,22 @@ "welcomeDocClose": "Close", "welcomeDocFailedToLoad": "Could not load documentation: {error}", "@welcomeDocFailedToLoad": { "placeholders": { "error": { "type": "String" } } }, + "welcomeChecklistHeader": "GETTING STARTED", + "welcomeChecklistBody": "Four steps to a working hub. Live state — refresh after changing anything.", + "welcomeChecklistAi": "Configure System AI", + "welcomeChecklistAiHint": "Settings → System AI. Required for the AI search and inline failure explanations.", + "welcomeChecklistMcp": "Add a public capability source", + "welcomeChecklistMcpHint": "Store → Today hero, or Settings → MCP Clients. DeepWiki and Semgrep are one click each.", + "welcomeChecklistModule": "Install a text module", + "welcomeChecklistModuleHint": "Store → search \"text\" → Install. Try text.extract for PDFs and DOCX.", + "welcomeChecklistFlow": "Run a saved flow", + "welcomeChecklistFlowHint": "Flows → pick one → Run. The bundled extract-summarize.yaml is a good first run.", + "welcomeChecklistAllDone": "All four steps complete.", + "welcomeChecklistDismiss": "Hide checklist", + "welcomeChecklistRefresh": "Refresh", + "welcomeChecklistRefreshing": "Checking…", + "welcomeChecklistDone": "Done", + "welcomeChecklistTodo": "Pending", "navDoctor": "Doctor", "navModules": "Modules", "navStore": "Store", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 9886204..6406d90 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -278,6 +278,102 @@ abstract class AppLocalizations { /// **'Could not load documentation: {error}'** String welcomeDocFailedToLoad(String error); + /// No description provided for @welcomeChecklistHeader. + /// + /// In en, this message translates to: + /// **'GETTING STARTED'** + String get welcomeChecklistHeader; + + /// No description provided for @welcomeChecklistBody. + /// + /// In en, this message translates to: + /// **'Four steps to a working hub. Live state — refresh after changing anything.'** + String get welcomeChecklistBody; + + /// No description provided for @welcomeChecklistAi. + /// + /// In en, this message translates to: + /// **'Configure System AI'** + String get welcomeChecklistAi; + + /// No description provided for @welcomeChecklistAiHint. + /// + /// In en, this message translates to: + /// **'Settings → System AI. Required for the AI search and inline failure explanations.'** + String get welcomeChecklistAiHint; + + /// No description provided for @welcomeChecklistMcp. + /// + /// In en, this message translates to: + /// **'Add a public capability source'** + String get welcomeChecklistMcp; + + /// No description provided for @welcomeChecklistMcpHint. + /// + /// In en, this message translates to: + /// **'Store → Today hero, or Settings → MCP Clients. DeepWiki and Semgrep are one click each.'** + String get welcomeChecklistMcpHint; + + /// No description provided for @welcomeChecklistModule. + /// + /// In en, this message translates to: + /// **'Install a text module'** + String get welcomeChecklistModule; + + /// No description provided for @welcomeChecklistModuleHint. + /// + /// In en, this message translates to: + /// **'Store → search \"text\" → Install. Try text.extract for PDFs and DOCX.'** + String get welcomeChecklistModuleHint; + + /// No description provided for @welcomeChecklistFlow. + /// + /// In en, this message translates to: + /// **'Run a saved flow'** + String get welcomeChecklistFlow; + + /// No description provided for @welcomeChecklistFlowHint. + /// + /// In en, this message translates to: + /// **'Flows → pick one → Run. The bundled extract-summarize.yaml is a good first run.'** + String get welcomeChecklistFlowHint; + + /// No description provided for @welcomeChecklistAllDone. + /// + /// In en, this message translates to: + /// **'All four steps complete.'** + String get welcomeChecklistAllDone; + + /// No description provided for @welcomeChecklistDismiss. + /// + /// In en, this message translates to: + /// **'Hide checklist'** + String get welcomeChecklistDismiss; + + /// No description provided for @welcomeChecklistRefresh. + /// + /// In en, this message translates to: + /// **'Refresh'** + String get welcomeChecklistRefresh; + + /// No description provided for @welcomeChecklistRefreshing. + /// + /// In en, this message translates to: + /// **'Checking…'** + String get welcomeChecklistRefreshing; + + /// No description provided for @welcomeChecklistDone. + /// + /// In en, this message translates to: + /// **'Done'** + String get welcomeChecklistDone; + + /// No description provided for @welcomeChecklistTodo. + /// + /// In en, this message translates to: + /// **'Pending'** + String get welcomeChecklistTodo; + /// No description provided for @navDoctor. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 2bab452..24c1976 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -112,6 +112,59 @@ class AppLocalizationsDe extends AppLocalizations { return 'Doku konnte nicht geladen werden: $error'; } + @override + String get welcomeChecklistHeader => 'ERSTE SCHRITTE'; + + @override + String get welcomeChecklistBody => + 'Vier Schritte zu einem funktionierenden Hub. Live-Zustand — nach jeder Änderung aktualisieren.'; + + @override + String get welcomeChecklistAi => 'System-AI konfigurieren'; + + @override + String get welcomeChecklistAiHint => + 'Einstellungen → System-AI. Wird für die KI-Suche und Fehlererklärungen gebraucht.'; + + @override + String get welcomeChecklistMcp => 'Öffentliche Capability-Quelle hinzufügen'; + + @override + String get welcomeChecklistMcpHint => + 'Store → Today-Hero, oder Einstellungen → MCP-Clients. DeepWiki und Semgrep sind je ein Klick.'; + + @override + String get welcomeChecklistModule => 'Ein Text-Modul installieren'; + + @override + String get welcomeChecklistModuleHint => + 'Store → nach „text\" suchen → Installieren. text.extract für PDFs und DOCX ist ein guter Start.'; + + @override + String get welcomeChecklistFlow => 'Einen gespeicherten Flow starten'; + + @override + String get welcomeChecklistFlowHint => + 'Flows → einen wählen → Starten. Der mitgelieferte extract-summarize.yaml ist ein guter erster Lauf.'; + + @override + String get welcomeChecklistAllDone => 'Alle vier Schritte erledigt.'; + + @override + String get welcomeChecklistDismiss => 'Checkliste ausblenden'; + + @override + String get welcomeChecklistRefresh => 'Aktualisieren'; + + @override + String get welcomeChecklistRefreshing => 'Prüfe…'; + + @override + String get welcomeChecklistDone => 'Erledigt'; + + @override + String get welcomeChecklistTodo => 'Offen'; + @override String get navDoctor => 'Diagnose'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 0cb196a..beb4c5d 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -112,6 +112,59 @@ class AppLocalizationsEn extends AppLocalizations { return 'Could not load documentation: $error'; } + @override + String get welcomeChecklistHeader => 'GETTING STARTED'; + + @override + String get welcomeChecklistBody => + 'Four steps to a working hub. Live state — refresh after changing anything.'; + + @override + String get welcomeChecklistAi => 'Configure System AI'; + + @override + String get welcomeChecklistAiHint => + 'Settings → System AI. Required for the AI search and inline failure explanations.'; + + @override + String get welcomeChecklistMcp => 'Add a public capability source'; + + @override + String get welcomeChecklistMcpHint => + 'Store → Today hero, or Settings → MCP Clients. DeepWiki and Semgrep are one click each.'; + + @override + String get welcomeChecklistModule => 'Install a text module'; + + @override + String get welcomeChecklistModuleHint => + 'Store → search \"text\" → Install. Try text.extract for PDFs and DOCX.'; + + @override + String get welcomeChecklistFlow => 'Run a saved flow'; + + @override + String get welcomeChecklistFlowHint => + 'Flows → pick one → Run. The bundled extract-summarize.yaml is a good first run.'; + + @override + String get welcomeChecklistAllDone => 'All four steps complete.'; + + @override + String get welcomeChecklistDismiss => 'Hide checklist'; + + @override + String get welcomeChecklistRefresh => 'Refresh'; + + @override + String get welcomeChecklistRefreshing => 'Checking…'; + + @override + String get welcomeChecklistDone => 'Done'; + + @override + String get welcomeChecklistTodo => 'Pending'; + @override String get navDoctor => 'Doctor'; diff --git a/lib/main.dart b/lib/main.dart index bc2c173..e4509c8 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -26,7 +26,7 @@ import 'widgets/widgets.dart'; /// Studio's own build version. Bump on every UI commit so the /// running app self-identifies — visible in the sidebar header /// and quick-glance proof that you're seeing the current build. -const String kStudioVersion = '0.38.0'; +const String kStudioVersion = '0.39.0'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); diff --git a/lib/pages/welcome.dart b/lib/pages/welcome.dart index 95a224e..1ffd36b 100644 --- a/lib/pages/welcome.dart +++ b/lib/pages/welcome.dart @@ -11,11 +11,14 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart' show rootBundle; import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import '../data/hub.dart'; import '../data/system_actions.dart'; import '../l10n/app_localizations.dart'; import '../theme/theme.dart'; import '../theme/tokens.dart'; +import '../widgets/widgets.dart'; class WelcomePage extends StatelessWidget { const WelcomePage({super.key}); @@ -45,6 +48,10 @@ class WelcomePage extends StatelessWidget { children: const [ _Hero(), SizedBox(height: FaiSpace.xxl), + // Checklist comes second — operator-actionable + // path beats educational content. Auto-hides + // once the operator dismisses it. + _OnboardingChecklist(), _SectionLabel(textKey: _SectionLabelKey.pillars), SizedBox(height: FaiSpace.md), _PillarRow(), @@ -359,6 +366,259 @@ class _TrustRow extends StatelessWidget { } } +/// SharedPreferences key the checklist persists "operator +/// dismissed me" under. Once true, the checklist is gone for +/// good — this is intentional; we don't want to nag operators +/// who chose a non-default path (e.g. CLI-only, no Studio +/// onboarding needed). +const String _kChecklistDismissedKey = 'welcome.checklist.dismissed'; + +/// Live getting-started checklist. Probes the running hub for +/// four signals that the operator has the basic loop wired up +/// (System AI configured, MCP source added, text module +/// installed, any flow run). Each signal is fetched in +/// parallel; failures stay false and the checklist still +/// renders (with the "Refresh" button for retry). +class _OnboardingChecklist extends StatefulWidget { + const _OnboardingChecklist(); + + @override + State<_OnboardingChecklist> createState() => _OnboardingChecklistState(); +} + +class _OnboardingChecklistState extends State<_OnboardingChecklist> { + bool _dismissed = false; + bool _initialised = false; + bool _refreshing = false; + bool _aiOk = false; + bool _mcpOk = false; + bool _moduleOk = false; + bool _flowOk = false; + + @override + void initState() { + super.initState(); + _bootstrap(); + } + + Future _bootstrap() async { + final prefs = await SharedPreferences.getInstance(); + if (!mounted) return; + setState(() { + _dismissed = prefs.getBool(_kChecklistDismissedKey) ?? false; + _initialised = true; + }); + if (!_dismissed) { + await _refresh(); + } + } + + Future _refresh() async { + setState(() => _refreshing = true); + final hub = HubService.instance; + // Probe each signal independently. A failure in one (e.g. + // hub temporarily unreachable) leaves the others + // computable; the checklist degrades gracefully. + final ai = hub + .systemAiStatus() + .then((s) => s.enabled) + .catchError((_) => false); + final mcp = hub + .listMcpClients() + .then((c) => c.isNotEmpty) + .catchError((_) => false); + final module = hub + .listModules() + .then((m) => m.any((x) => x.name.startsWith('text.'))) + .catchError((_) => false); + final flow = hub + .recentEvents(limit: 100) + .then((events) => events.any((e) => e.type == 'flow.completed')) + .catchError((_) => false); + final results = await Future.wait([ai, mcp, module, flow]); + if (!mounted) return; + setState(() { + _refreshing = false; + _aiOk = results[0]; + _mcpOk = results[1]; + _moduleOk = results[2]; + _flowOk = results[3]; + }); + } + + Future _dismiss() async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool(_kChecklistDismissedKey, true); + if (!mounted) return; + setState(() => _dismissed = true); + } + + @override + Widget build(BuildContext context) { + if (!_initialised || _dismissed) return const SizedBox.shrink(); + final theme = Theme.of(context); + final l = AppLocalizations.of(context)!; + final allDone = _aiOk && _mcpOk && _moduleOk && _flowOk; + final items = <(bool, String, String)>[ + (_aiOk, l.welcomeChecklistAi, l.welcomeChecklistAiHint), + (_mcpOk, l.welcomeChecklistMcp, l.welcomeChecklistMcpHint), + (_moduleOk, l.welcomeChecklistModule, l.welcomeChecklistModuleHint), + (_flowOk, l.welcomeChecklistFlow, l.welcomeChecklistFlowHint), + ]; + return Padding( + padding: const EdgeInsets.only(bottom: FaiSpace.xxl), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + l.welcomeChecklistHeader, + style: theme.textTheme.labelSmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + letterSpacing: 0.6, + fontSize: 10, + ), + ), + const Spacer(), + IconButton( + icon: _refreshing + ? const SizedBox( + width: 14, + height: 14, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.refresh, size: 16), + visualDensity: VisualDensity.compact, + tooltip: _refreshing + ? l.welcomeChecklistRefreshing + : l.welcomeChecklistRefresh, + onPressed: _refreshing ? null : _refresh, + ), + ], + ), + const SizedBox(height: 4), + Text( + l.welcomeChecklistBody, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: FaiSpace.md), + Container( + decoration: BoxDecoration( + color: theme.colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(FaiRadius.md), + border: Border.all(color: theme.colorScheme.outlineVariant), + ), + child: Column( + children: [ + for (var i = 0; i < items.length; i++) ...[ + if (i > 0) + Divider( + height: 1, + color: theme.colorScheme.outlineVariant, + ), + _ChecklistRow( + done: items[i].$1, + title: items[i].$2, + hint: items[i].$3, + ), + ], + ], + ), + ), + if (allDone) ...[ + const SizedBox(height: FaiSpace.md), + Row( + children: [ + Icon( + Icons.check_circle, + size: 16, + color: FaiColors.success, + ), + const SizedBox(width: FaiSpace.xs), + Text( + l.welcomeChecklistAllDone, + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + const Spacer(), + FilledButton.tonal( + onPressed: _dismiss, + child: Text(l.welcomeChecklistDismiss), + ), + ], + ), + ], + ], + ), + ); + } +} + +class _ChecklistRow extends StatelessWidget { + final bool done; + final String title; + final String hint; + + const _ChecklistRow({ + required this.done, + required this.title, + required this.hint, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final l = AppLocalizations.of(context)!; + return Padding( + padding: const EdgeInsets.all(FaiSpace.lg), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon( + done ? Icons.check_circle : Icons.radio_button_unchecked, + size: 20, + color: done ? FaiColors.success : theme.colorScheme.outline, + ), + const SizedBox(width: FaiSpace.lg), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w600, + color: done + ? theme.colorScheme.onSurfaceVariant + : theme.colorScheme.onSurface, + decoration: done ? TextDecoration.lineThrough : null, + ), + ), + const SizedBox(height: 2), + Text( + hint, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + const SizedBox(width: FaiSpace.md), + FaiPill( + label: done ? l.welcomeChecklistDone : l.welcomeChecklistTodo, + tone: done ? FaiPillTone.success : FaiPillTone.neutral, + ), + ], + ), + ); + } +} + /// Doc-card entries — each links to a bundled markdown file /// under `assets/docs/`. The slug is the filename stem; the /// loader picks `.md` or `_de.md` per locale. diff --git a/pubspec.yaml b/pubspec.yaml index 140dd44..03a6510 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: fai_studio description: "F∆I Studio — desktop GUI for the F∆I hub" publish_to: 'none' -version: 0.38.0 +version: 0.39.0 environment: sdk: ^3.11.0-200.1.beta