feat(studio): plain-language store, bottom-anchored ask bar (v0.33.0)
The store had three structural problems flagged in user feedback: 1. "Lernkurve zu hoch — alle bridge, debug, alpha, planned, keiner weiß was das heißt." Wire-level enum values bled through into the UI: `bridge`, `debug`, `published`, `alpha`, `planned`. Operators don't share the data model. 2. "Filter / Kategorien sollten oben in die Leiste neben Store zum ausklappen, der Body soll übersichtlicher werden." A chip-row plus category strip plus result-count line ate a full row of viewport on every screen. 3. "Suche passt besser an den unteren Bildschirmrand." The chat-style ask bar belongs at the bottom — Claude / ChatGPT / Slack pattern — not at the top where it competes with the Today hero. Plus: "Es gibt noch ganz viele overflows in den Beschreibungen." Changes: - Plain-language display names. New ARB keys for category labels (Connectors / Sample modules / AI models / Storage / Channels / Authentication / Orchestration / …) and updated status values (stable / experimental / coming soon). Wire ids stay in StoreEntry, RPCs, seed.yaml — only the rendered pills and dropdowns get translated. Helpers `_categoryDisplayName` and `_statusDisplayName` keep the mapping in one place. - Toolbar moves into the AppBar. The new `_CategoryDropdown` hosts the category picker as a popup-menu with localised labels; the existing `_FilterButton` lives next to it; the reload icon stays where it always was. The body no longer carries any chip strip or result-count line. - Bottom-anchored `_AskBar`. The Scaffold body becomes a Column of `Expanded(scrollable content)` plus a pinned composer row at the foot of the viewport, with a top divider matching the chat-input pattern of modern AI assistants. AI answers now render at the top of the scroll (above the Today-Hero) so the operator sees them right after submitting the question at the bottom. - Overflow sweep on descriptions. Today-Hero header row becomes a Wrap (badge + deck flow naturally on narrow windows), the title is bound to 3 lines, the body to 6. Card category text gets `maxLines: 1, ellipsis` in both StoreCard and FeaturedTile. Recommended-source card title gets the same treatment. - Better toast for the "added but zero capabilities" path. When MCP discovery succeeds but returns no tools (typical Streamable-HTTP servers without Mcp-Session-Id support, or servers that need `notifications/initialized`), the toast now explains the situation in plain language and points the operator at Settings → MCP Clients to retry. Old pluralised toast still fires when N > 0. Hub-side `notifications/initialized` for HTTP MCP and Mcp-Session-Id support are out of scope for this commit; tracked as a separate fai_hub follow-up. Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
parent
c7869870c4
commit
de22791e86
8 changed files with 434 additions and 143 deletions
|
|
@ -127,10 +127,22 @@
|
||||||
"storeNoMatchesHint": "Filter anpassen oder Suche leeren, um alle Einträge zu sehen.",
|
"storeNoMatchesHint": "Filter anpassen oder Suche leeren, um alle Einträge zu sehen.",
|
||||||
"storeClearFilters": "Filter zurücksetzen",
|
"storeClearFilters": "Filter zurücksetzen",
|
||||||
"storeStatusAll": "Alle",
|
"storeStatusAll": "Alle",
|
||||||
"storeStatusPublished": "Veröffentlicht",
|
"storeStatusPublished": "stabil",
|
||||||
"storeStatusAlpha": "Alpha",
|
"storeStatusAlpha": "experimentell",
|
||||||
"storeStatusPlanned": "Geplant",
|
"storeStatusPlanned": "geplant",
|
||||||
"storeInstalledOnly": "Installiert",
|
"storeInstalledOnly": "Installiert",
|
||||||
|
"storeCategoryLabel": "Kategorie",
|
||||||
|
"storeCategoryAll": "Alle Kategorien",
|
||||||
|
"storeCategoryBridge": "Verbindungen",
|
||||||
|
"storeCategoryDebug": "Beispiele",
|
||||||
|
"storeCategoryText": "Text",
|
||||||
|
"storeCategoryLlm": "KI-Modelle",
|
||||||
|
"storeCategorySystem": "System",
|
||||||
|
"storeCategoryAuth": "Authentifizierung",
|
||||||
|
"storeCategoryStorage": "Speicher",
|
||||||
|
"storeCategoryChannel": "Kanäle",
|
||||||
|
"storeCategoryWeb": "Web",
|
||||||
|
"storeCategoryOrchestrator": "Orchestrierung",
|
||||||
"storeNResults": "{n} Treffer",
|
"storeNResults": "{n} Treffer",
|
||||||
"@storeNResults": {
|
"@storeNResults": {
|
||||||
"placeholders": { "n": { "type": "int" } }
|
"placeholders": { "n": { "type": "int" } }
|
||||||
|
|
@ -221,6 +233,10 @@
|
||||||
"n": { "type": "int" }
|
"n": { "type": "int" }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"storeProviderAddedZeroToast": "{name} ist eingetragen, aber der Server hat noch keine Capabilities geliefert. Öffne Einstellungen → MCP-Clients, um Discovery erneut zu versuchen oder den Server zu prüfen.",
|
||||||
|
"@storeProviderAddedZeroToast": {
|
||||||
|
"placeholders": { "name": { "type": "String" } }
|
||||||
|
},
|
||||||
"storeProviderAddFailed": "{name} konnte nicht hinzugefügt werden: {error}",
|
"storeProviderAddFailed": "{name} konnte nicht hinzugefügt werden: {error}",
|
||||||
"@storeProviderAddFailed": {
|
"@storeProviderAddFailed": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
|
|
|
||||||
|
|
@ -128,10 +128,22 @@
|
||||||
"storeNoMatchesHint": "Adjust the filters or clear the search to see all entries.",
|
"storeNoMatchesHint": "Adjust the filters or clear the search to see all entries.",
|
||||||
"storeClearFilters": "Clear filters",
|
"storeClearFilters": "Clear filters",
|
||||||
"storeStatusAll": "All",
|
"storeStatusAll": "All",
|
||||||
"storeStatusPublished": "Published",
|
"storeStatusPublished": "stable",
|
||||||
"storeStatusAlpha": "Alpha",
|
"storeStatusAlpha": "experimental",
|
||||||
"storeStatusPlanned": "Planned",
|
"storeStatusPlanned": "coming soon",
|
||||||
"storeInstalledOnly": "Installed",
|
"storeInstalledOnly": "Installed",
|
||||||
|
"storeCategoryLabel": "Category",
|
||||||
|
"storeCategoryAll": "All categories",
|
||||||
|
"storeCategoryBridge": "Connectors",
|
||||||
|
"storeCategoryDebug": "Sample modules",
|
||||||
|
"storeCategoryText": "Text",
|
||||||
|
"storeCategoryLlm": "AI models",
|
||||||
|
"storeCategorySystem": "System",
|
||||||
|
"storeCategoryAuth": "Authentication",
|
||||||
|
"storeCategoryStorage": "Storage",
|
||||||
|
"storeCategoryChannel": "Channels",
|
||||||
|
"storeCategoryWeb": "Web",
|
||||||
|
"storeCategoryOrchestrator": "Orchestration",
|
||||||
"storeNResults": "{n} result{n, plural, =1{} other{s}}",
|
"storeNResults": "{n} result{n, plural, =1{} other{s}}",
|
||||||
"@storeNResults": {
|
"@storeNResults": {
|
||||||
"placeholders": { "n": { "type": "int" } }
|
"placeholders": { "n": { "type": "int" } }
|
||||||
|
|
@ -222,6 +234,10 @@
|
||||||
"n": { "type": "int" }
|
"n": { "type": "int" }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"storeProviderAddedZeroToast": "{name} registered, but the server returned no capabilities yet. Open Settings → MCP Clients to retry discovery or check the server.",
|
||||||
|
"@storeProviderAddedZeroToast": {
|
||||||
|
"placeholders": { "name": { "type": "String" } }
|
||||||
|
},
|
||||||
"storeProviderAddFailed": "Could not add {name}: {error}",
|
"storeProviderAddFailed": "Could not add {name}: {error}",
|
||||||
"@storeProviderAddFailed": {
|
"@storeProviderAddFailed": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
|
|
|
||||||
|
|
@ -761,19 +761,19 @@ abstract class AppLocalizations {
|
||||||
/// No description provided for @storeStatusPublished.
|
/// No description provided for @storeStatusPublished.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Published'**
|
/// **'stable'**
|
||||||
String get storeStatusPublished;
|
String get storeStatusPublished;
|
||||||
|
|
||||||
/// No description provided for @storeStatusAlpha.
|
/// No description provided for @storeStatusAlpha.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Alpha'**
|
/// **'experimental'**
|
||||||
String get storeStatusAlpha;
|
String get storeStatusAlpha;
|
||||||
|
|
||||||
/// No description provided for @storeStatusPlanned.
|
/// No description provided for @storeStatusPlanned.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Planned'**
|
/// **'coming soon'**
|
||||||
String get storeStatusPlanned;
|
String get storeStatusPlanned;
|
||||||
|
|
||||||
/// No description provided for @storeInstalledOnly.
|
/// No description provided for @storeInstalledOnly.
|
||||||
|
|
@ -782,6 +782,78 @@ abstract class AppLocalizations {
|
||||||
/// **'Installed'**
|
/// **'Installed'**
|
||||||
String get storeInstalledOnly;
|
String get storeInstalledOnly;
|
||||||
|
|
||||||
|
/// No description provided for @storeCategoryLabel.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Category'**
|
||||||
|
String get storeCategoryLabel;
|
||||||
|
|
||||||
|
/// No description provided for @storeCategoryAll.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'All categories'**
|
||||||
|
String get storeCategoryAll;
|
||||||
|
|
||||||
|
/// No description provided for @storeCategoryBridge.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Connectors'**
|
||||||
|
String get storeCategoryBridge;
|
||||||
|
|
||||||
|
/// No description provided for @storeCategoryDebug.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Sample modules'**
|
||||||
|
String get storeCategoryDebug;
|
||||||
|
|
||||||
|
/// No description provided for @storeCategoryText.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Text'**
|
||||||
|
String get storeCategoryText;
|
||||||
|
|
||||||
|
/// No description provided for @storeCategoryLlm.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'AI models'**
|
||||||
|
String get storeCategoryLlm;
|
||||||
|
|
||||||
|
/// No description provided for @storeCategorySystem.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'System'**
|
||||||
|
String get storeCategorySystem;
|
||||||
|
|
||||||
|
/// No description provided for @storeCategoryAuth.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Authentication'**
|
||||||
|
String get storeCategoryAuth;
|
||||||
|
|
||||||
|
/// No description provided for @storeCategoryStorage.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Storage'**
|
||||||
|
String get storeCategoryStorage;
|
||||||
|
|
||||||
|
/// No description provided for @storeCategoryChannel.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Channels'**
|
||||||
|
String get storeCategoryChannel;
|
||||||
|
|
||||||
|
/// No description provided for @storeCategoryWeb.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Web'**
|
||||||
|
String get storeCategoryWeb;
|
||||||
|
|
||||||
|
/// No description provided for @storeCategoryOrchestrator.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Orchestration'**
|
||||||
|
String get storeCategoryOrchestrator;
|
||||||
|
|
||||||
/// No description provided for @storeNResults.
|
/// No description provided for @storeNResults.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
@ -1100,6 +1172,12 @@ abstract class AppLocalizations {
|
||||||
/// **'Added {name} — {n} federated {n, plural, =1{capability} other{capabilities}}.'**
|
/// **'Added {name} — {n} federated {n, plural, =1{capability} other{capabilities}}.'**
|
||||||
String storeProviderAddedToast(String name, int n);
|
String storeProviderAddedToast(String name, int n);
|
||||||
|
|
||||||
|
/// No description provided for @storeProviderAddedZeroToast.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'{name} registered, but the server returned no capabilities yet. Open Settings → MCP Clients to retry discovery or check the server.'**
|
||||||
|
String storeProviderAddedZeroToast(String name);
|
||||||
|
|
||||||
/// No description provided for @storeProviderAddFailed.
|
/// No description provided for @storeProviderAddFailed.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
|
||||||
|
|
@ -382,17 +382,53 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
String get storeStatusAll => 'Alle';
|
String get storeStatusAll => 'Alle';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get storeStatusPublished => 'Veröffentlicht';
|
String get storeStatusPublished => 'stabil';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get storeStatusAlpha => 'Alpha';
|
String get storeStatusAlpha => 'experimentell';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get storeStatusPlanned => 'Geplant';
|
String get storeStatusPlanned => 'geplant';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get storeInstalledOnly => 'Installiert';
|
String get storeInstalledOnly => 'Installiert';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get storeCategoryLabel => 'Kategorie';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get storeCategoryAll => 'Alle Kategorien';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get storeCategoryBridge => 'Verbindungen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get storeCategoryDebug => 'Beispiele';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get storeCategoryText => 'Text';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get storeCategoryLlm => 'KI-Modelle';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get storeCategorySystem => 'System';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get storeCategoryAuth => 'Authentifizierung';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get storeCategoryStorage => 'Speicher';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get storeCategoryChannel => 'Kanäle';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get storeCategoryWeb => 'Web';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get storeCategoryOrchestrator => 'Orchestrierung';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String storeNResults(int n) {
|
String storeNResults(int n) {
|
||||||
return '$n Treffer';
|
return '$n Treffer';
|
||||||
|
|
@ -596,6 +632,11 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
return '$name hinzugefügt — $n $_temp0.';
|
return '$name hinzugefügt — $n $_temp0.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String storeProviderAddedZeroToast(String name) {
|
||||||
|
return '$name ist eingetragen, aber der Server hat noch keine Capabilities geliefert. Öffne Einstellungen → MCP-Clients, um Discovery erneut zu versuchen oder den Server zu prüfen.';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String storeProviderAddFailed(String name, String error) {
|
String storeProviderAddFailed(String name, String error) {
|
||||||
return '$name konnte nicht hinzugefügt werden: $error';
|
return '$name konnte nicht hinzugefügt werden: $error';
|
||||||
|
|
|
||||||
|
|
@ -394,17 +394,53 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
String get storeStatusAll => 'All';
|
String get storeStatusAll => 'All';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get storeStatusPublished => 'Published';
|
String get storeStatusPublished => 'stable';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get storeStatusAlpha => 'Alpha';
|
String get storeStatusAlpha => 'experimental';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get storeStatusPlanned => 'Planned';
|
String get storeStatusPlanned => 'coming soon';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get storeInstalledOnly => 'Installed';
|
String get storeInstalledOnly => 'Installed';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get storeCategoryLabel => 'Category';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get storeCategoryAll => 'All categories';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get storeCategoryBridge => 'Connectors';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get storeCategoryDebug => 'Sample modules';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get storeCategoryText => 'Text';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get storeCategoryLlm => 'AI models';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get storeCategorySystem => 'System';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get storeCategoryAuth => 'Authentication';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get storeCategoryStorage => 'Storage';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get storeCategoryChannel => 'Channels';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get storeCategoryWeb => 'Web';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get storeCategoryOrchestrator => 'Orchestration';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String storeNResults(int n) {
|
String storeNResults(int n) {
|
||||||
String _temp0 = intl.Intl.pluralLogic(
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
|
|
@ -613,6 +649,11 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
return 'Added $name — $n federated $_temp0.';
|
return 'Added $name — $n federated $_temp0.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String storeProviderAddedZeroToast(String name) {
|
||||||
|
return '$name registered, but the server returned no capabilities yet. Open Settings → MCP Clients to retry discovery or check the server.';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String storeProviderAddFailed(String name, String error) {
|
String storeProviderAddFailed(String name, String error) {
|
||||||
return 'Could not add $name: $error';
|
return 'Could not add $name: $error';
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ import 'widgets/widgets.dart';
|
||||||
/// Studio's own build version. Bump on every UI commit so the
|
/// Studio's own build version. Bump on every UI commit so the
|
||||||
/// running app self-identifies — visible in the sidebar header
|
/// running app self-identifies — visible in the sidebar header
|
||||||
/// and quick-glance proof that you're seeing the current build.
|
/// and quick-glance proof that you're seeing the current build.
|
||||||
const String kStudioVersion = '0.32.0';
|
const String kStudioVersion = '0.33.0';
|
||||||
|
|
||||||
Future<void> main() async {
|
Future<void> main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
|
||||||
|
|
@ -153,10 +153,34 @@ class _StorePageState extends State<StorePage> {
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(l.searchGroupStore),
|
title: Text(l.searchGroupStore),
|
||||||
actions: [
|
actions: [
|
||||||
// The DE/EN toggle now lives in the sidebar footer
|
// Category dropdown and Filter button live in the
|
||||||
// and flips the entire app's locale. The Store
|
// toolbar so the body stays uncluttered. Both refresh
|
||||||
// detail-sheet content follows automatically because
|
// off the current store snapshot via FutureBuilder.
|
||||||
// _locale reads from `Localizations.localeOf`.
|
FutureBuilder<List<StoreItem>>(
|
||||||
|
future: _future,
|
||||||
|
builder: (ctx, snap) {
|
||||||
|
final raw = snap.data ?? const <StoreItem>[];
|
||||||
|
final categories = _categoriesIn(raw);
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(right: FaiSpace.sm),
|
||||||
|
child: _CategoryDropdown(
|
||||||
|
categories: categories,
|
||||||
|
selected: _category,
|
||||||
|
onSelected: (v) {
|
||||||
|
setState(() => _category = v);
|
||||||
|
_runSearch();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(right: FaiSpace.sm),
|
||||||
|
child: _FilterButton(
|
||||||
|
activeCount: _activeFilterCount(),
|
||||||
|
onPressed: _openFilterDialog,
|
||||||
|
),
|
||||||
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.refresh, size: 18),
|
icon: const Icon(Icons.refresh, size: 18),
|
||||||
tooltip: l.storeReloadTooltip,
|
tooltip: l.storeReloadTooltip,
|
||||||
|
|
@ -165,70 +189,20 @@ class _StorePageState extends State<StorePage> {
|
||||||
const SizedBox(width: FaiSpace.sm),
|
const SizedBox(width: FaiSpace.sm),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: Padding(
|
body: Column(
|
||||||
padding: const EdgeInsets.fromLTRB(
|
children: [
|
||||||
FaiSpace.xl,
|
// Scrollable content: editorial chrome + grid. Sits in
|
||||||
FaiSpace.lg,
|
// an Expanded so the bottom AskBar pins to the foot of
|
||||||
FaiSpace.xl,
|
// the viewport, mirroring the chat-input pattern of
|
||||||
0,
|
// Claude / ChatGPT / Slack.
|
||||||
),
|
Expanded(
|
||||||
child: Column(
|
child: Padding(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
padding: const EdgeInsets.fromLTRB(
|
||||||
children: [
|
FaiSpace.xl,
|
||||||
_AskBar(
|
FaiSpace.lg,
|
||||||
controller: _queryCtrl,
|
FaiSpace.xl,
|
||||||
thinking: _aiThinking,
|
0,
|
||||||
aiAvailable: _systemAiEnabled,
|
),
|
||||||
onSubmit: _onAskSubmit,
|
|
||||||
onClear: _clearQuery,
|
|
||||||
onTextChange: _onAskTyping,
|
|
||||||
),
|
|
||||||
const SizedBox(height: FaiSpace.sm),
|
|
||||||
FutureBuilder<List<StoreItem>>(
|
|
||||||
future: _future,
|
|
||||||
builder: (context, snap) {
|
|
||||||
final raw = snap.data ?? const <StoreItem>[];
|
|
||||||
final items = _applyAllFilters(raw);
|
|
||||||
final categories = _categoriesIn(raw);
|
|
||||||
final activeFilters = _activeFilterCount();
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
_FilterButton(
|
|
||||||
activeCount: activeFilters,
|
|
||||||
onPressed: _openFilterDialog,
|
|
||||||
),
|
|
||||||
const SizedBox(width: FaiSpace.md),
|
|
||||||
if (categories.isNotEmpty)
|
|
||||||
Expanded(
|
|
||||||
child: _CategoryStrip(
|
|
||||||
categories: categories,
|
|
||||||
selected: _category,
|
|
||||||
onSelected: (v) {
|
|
||||||
setState(() => _category = v);
|
|
||||||
_runSearch();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
else
|
|
||||||
const Spacer(),
|
|
||||||
Text(
|
|
||||||
l.storeNResults(items.length),
|
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(height: FaiSpace.md),
|
|
||||||
Expanded(
|
|
||||||
child: FutureBuilder<List<StoreItem>>(
|
child: FutureBuilder<List<StoreItem>>(
|
||||||
future: _future,
|
future: _future,
|
||||||
builder: (context, snap) {
|
builder: (context, snap) {
|
||||||
|
|
@ -299,6 +273,18 @@ class _StorePageState extends State<StorePage> {
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
if (_aiThinking ||
|
||||||
|
_aiAnswer != null ||
|
||||||
|
_aiError != null) ...[
|
||||||
|
_AiAnswerCard(
|
||||||
|
thinking: _aiThinking,
|
||||||
|
answer: _aiAnswer,
|
||||||
|
matches: _aiMatches ?? const <_AiMatch>[],
|
||||||
|
error: _aiError,
|
||||||
|
onClear: _clearQuery,
|
||||||
|
),
|
||||||
|
const SizedBox(height: FaiSpace.lg),
|
||||||
|
],
|
||||||
if (showToday) ...[
|
if (showToday) ...[
|
||||||
_StoreTodayHero(
|
_StoreTodayHero(
|
||||||
story: _todayStory,
|
story: _todayStory,
|
||||||
|
|
@ -333,18 +319,6 @@ class _StorePageState extends State<StorePage> {
|
||||||
),
|
),
|
||||||
const SizedBox(height: FaiSpace.lg),
|
const SizedBox(height: FaiSpace.lg),
|
||||||
],
|
],
|
||||||
if (_aiThinking ||
|
|
||||||
_aiAnswer != null ||
|
|
||||||
_aiError != null) ...[
|
|
||||||
_AiAnswerCard(
|
|
||||||
thinking: _aiThinking,
|
|
||||||
answer: _aiAnswer,
|
|
||||||
matches: _aiMatches ?? const <_AiMatch>[],
|
|
||||||
error: _aiError,
|
|
||||||
onClear: _clearQuery,
|
|
||||||
),
|
|
||||||
const SizedBox(height: FaiSpace.lg),
|
|
||||||
],
|
|
||||||
_StoreGrid(
|
_StoreGrid(
|
||||||
items: items,
|
items: items,
|
||||||
locale: _locale,
|
locale: _locale,
|
||||||
|
|
@ -352,14 +326,39 @@ class _StorePageState extends State<StorePage> {
|
||||||
onTap: _openDetail,
|
onTap: _openDetail,
|
||||||
onInstall: _install,
|
onInstall: _install,
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: FaiSpace.lg),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
// Pinned chat-style ask bar at the foot of the
|
||||||
|
// viewport. Always visible so the operator can switch
|
||||||
|
// from browsing to asking with one click — same shape
|
||||||
|
// as Claude / ChatGPT / Slack composer rows.
|
||||||
|
Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.colorScheme.surface,
|
||||||
|
border: Border(
|
||||||
|
top: BorderSide(color: theme.colorScheme.outlineVariant),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: FaiSpace.xl,
|
||||||
|
vertical: FaiSpace.md,
|
||||||
|
),
|
||||||
|
child: _AskBar(
|
||||||
|
controller: _queryCtrl,
|
||||||
|
thinking: _aiThinking,
|
||||||
|
aiAvailable: _systemAiEnabled,
|
||||||
|
onSubmit: _onAskSubmit,
|
||||||
|
onClear: _clearQuery,
|
||||||
|
onTextChange: _onAskTyping,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -532,11 +531,20 @@ class _StorePageState extends State<StorePage> {
|
||||||
orElse: () => list.last,
|
orElse: () => list.last,
|
||||||
);
|
);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
// toolCount == 0 happens when the server registered fine
|
||||||
|
// but discovery returned nothing — typically a transport
|
||||||
|
// mismatch or a server that wants `notifications/initialized`
|
||||||
|
// before listing. We surface the difference so the
|
||||||
|
// operator knows whether to celebrate or to debug.
|
||||||
|
final msg = added.toolCount > 0
|
||||||
|
? l.storeProviderAddedToast(s.label, added.toolCount)
|
||||||
|
: l.storeProviderAddedZeroToast(s.label);
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(
|
content: Text(msg),
|
||||||
l.storeProviderAddedToast(s.label, added.toolCount),
|
duration: added.toolCount > 0
|
||||||
),
|
? const Duration(seconds: 4)
|
||||||
|
: const Duration(seconds: 8),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
_runSearch();
|
_runSearch();
|
||||||
|
|
@ -565,6 +573,59 @@ String _sourceForItem(StoreItem i) {
|
||||||
return 'federated';
|
return 'federated';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Translates a wire-level category id (`bridge`, `debug`, …)
|
||||||
|
/// into a label the operator can read. Wire ids stay in the
|
||||||
|
/// data model — only the display side gets localised. Unknown
|
||||||
|
/// ids fall through unchanged so newly-added categories don't
|
||||||
|
/// vanish until we update this map.
|
||||||
|
String _categoryDisplayName(BuildContext ctx, String wire) {
|
||||||
|
final l = AppLocalizations.of(ctx)!;
|
||||||
|
switch (wire) {
|
||||||
|
case '':
|
||||||
|
return '';
|
||||||
|
case 'bridge':
|
||||||
|
return l.storeCategoryBridge;
|
||||||
|
case 'debug':
|
||||||
|
return l.storeCategoryDebug;
|
||||||
|
case 'text':
|
||||||
|
case 'text-processing':
|
||||||
|
return l.storeCategoryText;
|
||||||
|
case 'llm':
|
||||||
|
return l.storeCategoryLlm;
|
||||||
|
case 'system':
|
||||||
|
return l.storeCategorySystem;
|
||||||
|
case 'auth':
|
||||||
|
return l.storeCategoryAuth;
|
||||||
|
case 'storage':
|
||||||
|
return l.storeCategoryStorage;
|
||||||
|
case 'channel':
|
||||||
|
return l.storeCategoryChannel;
|
||||||
|
case 'web':
|
||||||
|
return l.storeCategoryWeb;
|
||||||
|
case 'orchestrator':
|
||||||
|
return l.storeCategoryOrchestrator;
|
||||||
|
default:
|
||||||
|
return wire;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Same idea for the `published` / `alpha` / `planned` status
|
||||||
|
/// values. Operators read "stable / experimental / coming
|
||||||
|
/// soon"; the wire keeps the strict enum it has always had.
|
||||||
|
String _statusDisplayName(BuildContext ctx, String wire) {
|
||||||
|
final l = AppLocalizations.of(ctx)!;
|
||||||
|
switch (wire) {
|
||||||
|
case 'published':
|
||||||
|
return l.storeStatusPublished;
|
||||||
|
case 'alpha':
|
||||||
|
return l.storeStatusAlpha;
|
||||||
|
case 'planned':
|
||||||
|
return l.storeStatusPlanned;
|
||||||
|
default:
|
||||||
|
return wire;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Multi-line ask-and-search input. Drives the toolbar's
|
/// Multi-line ask-and-search input. Drives the toolbar's
|
||||||
/// primary action: substring keyword search on every keystroke
|
/// primary action: substring keyword search on every keystroke
|
||||||
/// (cheap, identical to the legacy `_SearchField`) plus an
|
/// (cheap, identical to the legacy `_SearchField`) plus an
|
||||||
|
|
@ -1056,15 +1117,17 @@ class _AiAnswerCard extends StatelessWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Horizontally-scrollable category chip strip. Always-on
|
/// Compact category picker that lives in the AppBar's actions
|
||||||
/// "All" chip first, then categories the current result set
|
/// row. Replaces the older horizontally-scrolling chip strip
|
||||||
/// contains. Disappears when the index is empty.
|
/// because chips ate a full row of viewport for what is in
|
||||||
class _CategoryStrip extends StatelessWidget {
|
/// practice a one-click operation; a dropdown is faster to
|
||||||
|
/// reach and never grows past the toolbar.
|
||||||
|
class _CategoryDropdown extends StatelessWidget {
|
||||||
final List<String> categories;
|
final List<String> categories;
|
||||||
final String selected;
|
final String selected;
|
||||||
final void Function(String) onSelected;
|
final ValueChanged<String> onSelected;
|
||||||
|
|
||||||
const _CategoryStrip({
|
const _CategoryDropdown({
|
||||||
required this.categories,
|
required this.categories,
|
||||||
required this.selected,
|
required this.selected,
|
||||||
required this.onSelected,
|
required this.onSelected,
|
||||||
|
|
@ -1072,25 +1135,53 @@ class _CategoryStrip extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
final l = AppLocalizations.of(context)!;
|
final l = AppLocalizations.of(context)!;
|
||||||
if (categories.isEmpty) return const SizedBox.shrink();
|
final activeLabel = selected.isEmpty
|
||||||
return SizedBox(
|
? l.storeCategoryAll
|
||||||
height: 36,
|
: _categoryDisplayName(context, selected);
|
||||||
child: ListView(
|
return PopupMenuButton<String>(
|
||||||
scrollDirection: Axis.horizontal,
|
tooltip: l.storeCategoryLabel,
|
||||||
children: [
|
initialValue: selected,
|
||||||
_ChoiceChip(
|
onSelected: onSelected,
|
||||||
label: l.storeStatusAll,
|
itemBuilder: (_) => [
|
||||||
selected: selected.isEmpty,
|
PopupMenuItem(
|
||||||
onSelected: () => onSelected(''),
|
value: '',
|
||||||
|
child: Text(l.storeCategoryAll),
|
||||||
|
),
|
||||||
|
const PopupMenuDivider(),
|
||||||
|
for (final c in categories)
|
||||||
|
PopupMenuItem(
|
||||||
|
value: c,
|
||||||
|
child: Text(_categoryDisplayName(context, c)),
|
||||||
),
|
),
|
||||||
for (final c in categories)
|
],
|
||||||
_ChoiceChip(
|
child: Container(
|
||||||
label: c,
|
padding: const EdgeInsets.symmetric(
|
||||||
selected: selected == c,
|
horizontal: FaiSpace.sm,
|
||||||
onSelected: () => onSelected(c),
|
vertical: 6,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(FaiRadius.sm),
|
||||||
|
border: Border.all(color: theme.colorScheme.outlineVariant),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.folder_outlined,
|
||||||
|
size: 14,
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
],
|
const SizedBox(width: 6),
|
||||||
|
Text(activeLabel, style: theme.textTheme.bodySmall),
|
||||||
|
Icon(
|
||||||
|
Icons.arrow_drop_down,
|
||||||
|
size: 16,
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -1392,7 +1483,9 @@ class _FeaturedTile extends StatelessWidget {
|
||||||
),
|
),
|
||||||
if (item.category.isNotEmpty)
|
if (item.category.isNotEmpty)
|
||||||
Text(
|
Text(
|
||||||
item.category,
|
_categoryDisplayName(context, item.category),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
|
|
@ -1424,7 +1517,7 @@ class _FeaturedTile extends StatelessWidget {
|
||||||
const SizedBox(width: FaiSpace.xs),
|
const SizedBox(width: FaiSpace.xs),
|
||||||
if (item.status.isNotEmpty)
|
if (item.status.isNotEmpty)
|
||||||
FaiPill(
|
FaiPill(
|
||||||
label: item.status,
|
label: _statusDisplayName(context, item.status),
|
||||||
tone: _toneForStatus(item.status),
|
tone: _toneForStatus(item.status),
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
|
|
@ -1523,7 +1616,11 @@ class _StoreCard extends StatelessWidget {
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
item.category.isEmpty ? '—' : item.category,
|
item.category.isEmpty
|
||||||
|
? '—'
|
||||||
|
: _categoryDisplayName(context, item.category),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
|
|
@ -1551,7 +1648,7 @@ class _StoreCard extends StatelessWidget {
|
||||||
)
|
)
|
||||||
else if (item.status.isNotEmpty)
|
else if (item.status.isNotEmpty)
|
||||||
FaiPill(
|
FaiPill(
|
||||||
label: item.status,
|
label: _statusDisplayName(context, item.status),
|
||||||
tone: _toneForStatus(item.status),
|
tone: _toneForStatus(item.status),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -1827,12 +1924,14 @@ class _StoreDetailSheetState extends State<_StoreDetailSheet> {
|
||||||
),
|
),
|
||||||
if (item.status.isNotEmpty)
|
if (item.status.isNotEmpty)
|
||||||
FaiPill(
|
FaiPill(
|
||||||
label: item.status,
|
label: _statusDisplayName(
|
||||||
|
context, item.status),
|
||||||
tone: _toneForStatus(item.status),
|
tone: _toneForStatus(item.status),
|
||||||
),
|
),
|
||||||
if (item.category.isNotEmpty)
|
if (item.category.isNotEmpty)
|
||||||
FaiPill(
|
FaiPill(
|
||||||
label: item.category,
|
label: _categoryDisplayName(
|
||||||
|
context, item.category),
|
||||||
tone: FaiPillTone.neutral,
|
tone: FaiPillTone.neutral,
|
||||||
),
|
),
|
||||||
if (item.installed)
|
if (item.installed)
|
||||||
|
|
@ -2664,13 +2763,15 @@ class _StoreTodayHero extends StatelessWidget {
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Wrap(
|
||||||
|
spacing: FaiSpace.sm,
|
||||||
|
runSpacing: 4,
|
||||||
|
crossAxisAlignment: WrapCrossAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
FaiPill(
|
FaiPill(
|
||||||
label: l.storeTodayBadge,
|
label: l.storeTodayBadge,
|
||||||
tone: FaiPillTone.accent,
|
tone: FaiPillTone.accent,
|
||||||
),
|
),
|
||||||
const SizedBox(width: FaiSpace.sm),
|
|
||||||
Text(
|
Text(
|
||||||
badge,
|
badge,
|
||||||
style: theme.textTheme.labelSmall?.copyWith(
|
style: theme.textTheme.labelSmall?.copyWith(
|
||||||
|
|
@ -2679,16 +2780,8 @@ class _StoreTodayHero extends StatelessWidget {
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: FaiSpace.sm),
|
|
||||||
Text(
|
Text(
|
||||||
'·',
|
'· ${l.storeTodayDeck}',
|
||||||
style: theme.textTheme.labelSmall?.copyWith(
|
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: FaiSpace.sm),
|
|
||||||
Text(
|
|
||||||
l.storeTodayDeck,
|
|
||||||
style: theme.textTheme.labelSmall?.copyWith(
|
style: theme.textTheme.labelSmall?.copyWith(
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
fontStyle: FontStyle.italic,
|
fontStyle: FontStyle.italic,
|
||||||
|
|
@ -2700,6 +2793,8 @@ class _StoreTodayHero extends StatelessWidget {
|
||||||
const SizedBox(height: FaiSpace.sm),
|
const SizedBox(height: FaiSpace.sm),
|
||||||
Text(
|
Text(
|
||||||
title,
|
title,
|
||||||
|
maxLines: 3,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
style: theme.textTheme.headlineSmall?.copyWith(
|
style: theme.textTheme.headlineSmall?.copyWith(
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
height: 1.15,
|
height: 1.15,
|
||||||
|
|
@ -2708,6 +2803,8 @@ class _StoreTodayHero extends StatelessWidget {
|
||||||
const SizedBox(height: FaiSpace.sm),
|
const SizedBox(height: FaiSpace.sm),
|
||||||
Text(
|
Text(
|
||||||
body,
|
body,
|
||||||
|
maxLines: 6,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
style: theme.textTheme.bodyMedium?.copyWith(
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
height: 1.45,
|
height: 1.45,
|
||||||
|
|
@ -2926,17 +3023,19 @@ class _RecommendedSourceCard extends StatelessWidget {
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
source.label,
|
source.label,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
style: theme.textTheme.bodyMedium?.copyWith(
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
desc,
|
desc,
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
maxLines: 2,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
name: fai_studio
|
name: fai_studio
|
||||||
description: "F∆I Studio — desktop GUI for the F∆I hub"
|
description: "F∆I Studio — desktop GUI for the F∆I hub"
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
version: 0.32.0
|
version: 0.33.0
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.11.0-200.1.beta
|
sdk: ^3.11.0-200.1.beta
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue