diff --git a/lib/data/hub.dart b/lib/data/hub.dart index 4cf5737..a46d1b7 100644 --- a/lib/data/hub.dart +++ b/lib/data/hub.dart @@ -5,6 +5,8 @@ // Methods return UI-friendly types so pages stay free of // protobuf imports. +import 'dart:typed_data'; + import 'package:fai_dart_sdk/fai_dart_sdk.dart'; import 'package:flutter/widgets.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -544,15 +546,20 @@ class HubService { ..sort((a, b) => a.name.compareTo(b.name)); } - /// Run a saved flow with the supplied text inputs. Returns - /// the named outputs as a map of UI-friendly strings. + /// Run a saved flow with the supplied inputs. Text values + /// land as text Payloads, byte values as bytes Payloads — + /// flows that mix both (e.g. extract taking a `document: + /// bytes` plus a text-shaped option) flow through one RPC. + /// Returns the named outputs as a map of UI-friendly strings. Future> runSavedFlow({ required String name, - required Map textInputs, + Map textInputs = const {}, + Map fileInputs = const {}, }) async { final r = await _client.runSavedFlow( name: name, textInputs: textInputs, + fileInputs: fileInputs, ); return { for (final entry in r.outputs.entries) diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index e9f1e81..b77e00a 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -389,8 +389,8 @@ "flowsNoneHint": "Flow speichern mit `fai admin flows save `, dann taucht er hier auf.", "flowsRunDialogTitle": "{name} ausführen", "@flowsRunDialogTitle": { "placeholders": { "name": { "type": "String" } } }, - "flowsRunDialogBody": "Eingaben als key=value, eine pro Zeile. Alle Werte werden als Text-Payloads gesendet. Für binäre Eingaben die `fai run` CLI nutzen.", - "flowsInputsHint": "name=World\ntarget_language=English\nsummary_style=three bullet points", + "flowsRunDialogBody": "Eingaben als key=value, eine pro Zeile. Klartext-Werte landen als Text. Wert mit `@` voranstellen liest die Datei als Bytes — wie `fai run --input document=@/pfad/zu/datei.pdf`.", + "flowsInputsHint": "name=Welt\ndocument=@/Users/ich/Dokumente/beispiel.pdf\nstil=drei Stichpunkte", "flowsRunButton": "Starten", "flowsRunningTitle": "{name} läuft", "@flowsRunningTitle": { "placeholders": { "name": { "type": "String" } } }, diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 7ee1b66..719a020 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -390,8 +390,8 @@ "flowsNoneHint": "Save a flow with `fai admin flows save ` and it shows up here.", "flowsRunDialogTitle": "Run {name}", "@flowsRunDialogTitle": { "placeholders": { "name": { "type": "String" } } }, - "flowsRunDialogBody": "Inputs as key=value, one per line. All values are sent as text payloads. For binary inputs use the `fai run` CLI.", - "flowsInputsHint": "name=World\ntarget_language=English\nsummary_style=three bullet points", + "flowsRunDialogBody": "Inputs as key=value, one per line. Plain values send as text. Prefix the value with `@` to read a file as bytes — same as `fai run --input document=@/path/to/file.pdf`.", + "flowsInputsHint": "name=World\ndocument=@/Users/me/Documents/sample.pdf\nstyle=three bullet points", "flowsRunButton": "Run", "flowsRunningTitle": "Running {name}", "@flowsRunningTitle": { "placeholders": { "name": { "type": "String" } } }, diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 2692df9..ba573ea 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -1823,13 +1823,13 @@ abstract class AppLocalizations { /// No description provided for @flowsRunDialogBody. /// /// In en, this message translates to: - /// **'Inputs as key=value, one per line. All values are sent as text payloads. For binary inputs use the `fai run` CLI.'** + /// **'Inputs as key=value, one per line. Plain values send as text. Prefix the value with `@` to read a file as bytes — same as `fai run --input document=@/path/to/file.pdf`.'** String get flowsRunDialogBody; /// No description provided for @flowsInputsHint. /// /// In en, this message translates to: - /// **'name=World\ntarget_language=English\nsummary_style=three bullet points'** + /// **'name=World\ndocument=@/Users/me/Documents/sample.pdf\nstyle=three bullet points'** String get flowsInputsHint; /// No description provided for @flowsRunButton. diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 4f23805..1aed18e 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -1025,11 +1025,11 @@ class AppLocalizationsDe extends AppLocalizations { @override String get flowsRunDialogBody => - 'Eingaben als key=value, eine pro Zeile. Alle Werte werden als Text-Payloads gesendet. Für binäre Eingaben die `fai run` CLI nutzen.'; + 'Eingaben als key=value, eine pro Zeile. Klartext-Werte landen als Text. Wert mit `@` voranstellen liest die Datei als Bytes — wie `fai run --input document=@/pfad/zu/datei.pdf`.'; @override String get flowsInputsHint => - 'name=World\ntarget_language=English\nsummary_style=three bullet points'; + 'name=Welt\ndocument=@/Users/ich/Dokumente/beispiel.pdf\nstil=drei Stichpunkte'; @override String get flowsRunButton => 'Starten'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index ff2943a..ddf7a66 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1040,11 +1040,11 @@ class AppLocalizationsEn extends AppLocalizations { @override String get flowsRunDialogBody => - 'Inputs as key=value, one per line. All values are sent as text payloads. For binary inputs use the `fai run` CLI.'; + 'Inputs as key=value, one per line. Plain values send as text. Prefix the value with `@` to read a file as bytes — same as `fai run --input document=@/path/to/file.pdf`.'; @override String get flowsInputsHint => - 'name=World\ntarget_language=English\nsummary_style=three bullet points'; + 'name=World\ndocument=@/Users/me/Documents/sample.pdf\nstyle=three bullet points'; @override String get flowsRunButton => 'Run'; diff --git a/lib/main.dart b/lib/main.dart index 93999ab..d95f640 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.39.1'; +const String kStudioVersion = '0.40.0'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); diff --git a/lib/pages/doctor.dart b/lib/pages/doctor.dart index b607337..9306495 100644 --- a/lib/pages/doctor.dart +++ b/lib/pages/doctor.dart @@ -734,22 +734,32 @@ class _ServicesPanel extends StatelessWidget { final theme = Theme.of(context); final l = AppLocalizations.of(context)!; if (snapshot.services.isEmpty) { + // Wrap on narrow windows so the mono-spaced hint + // (`add to ~/.fai/config.yaml under services:`) does not + // overflow horizontally past the card's right edge. return FaiCard( - child: Row( + child: Wrap( + spacing: FaiSpace.sm, + runSpacing: FaiSpace.xs, + crossAxisAlignment: WrapCrossAlignment.center, children: [ - Icon( - Icons.dns_outlined, - size: 18, - color: theme.colorScheme.onSurfaceVariant, + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.dns_outlined, + size: 18, + color: theme.colorScheme.onSurfaceVariant, + ), + const SizedBox(width: FaiSpace.sm), + Text( + l.doctorServicesEmpty, + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ], ), - const SizedBox(width: FaiSpace.sm), - Text( - l.doctorServicesEmpty, - style: theme.textTheme.bodyMedium?.copyWith( - color: theme.colorScheme.onSurfaceVariant, - ), - ), - const Spacer(), Text( l.doctorServicesEmptyHint, style: FaiTheme.mono( diff --git a/lib/pages/flows.dart b/lib/pages/flows.dart index 3efa1f8..c7a9187 100644 --- a/lib/pages/flows.dart +++ b/lib/pages/flows.dart @@ -1,3 +1,6 @@ +import 'dart:io'; +import 'dart:typed_data'; + import 'package:flutter/material.dart'; import '../data/hub.dart'; @@ -29,11 +32,44 @@ class _FlowsPageState extends State { Future _runFlow(SavedFlow flow) async { final inputs = await _FlowInputDialog.show(context, flow); if (inputs == null) return; + // Split the dialog's text-shaped key=value pairs into text + // vs file payloads. Values starting with `@` mean "treat + // the rest as a path; read the file as bytes" — same + // syntax `fai run --input` uses on the CLI. File-read + // failures bubble up as the run dialog's error state so + // the operator sees a useful message. + final textInputs = {}; + final fileInputs = {}; + String? readError; + for (final entry in inputs.entries) { + final value = entry.value; + if (value.startsWith('@')) { + final path = value.substring(1).trim(); + try { + fileInputs[entry.key] = await File(path).readAsBytes(); + } catch (e) { + readError = '${entry.key}: $e'; + break; + } + } else { + textInputs[entry.key] = value; + } + } if (!mounted) return; + if (readError != null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(readError)), + ); + return; + } showDialog( context: context, barrierDismissible: false, - builder: (_) => _FlowRunDialog(flow: flow, inputs: inputs), + builder: (_) => _FlowRunDialog( + flow: flow, + textInputs: textInputs, + fileInputs: fileInputs, + ), ); } @@ -248,9 +284,14 @@ class _FlowInputDialogState extends State<_FlowInputDialog> { class _FlowRunDialog extends StatefulWidget { final SavedFlow flow; - final Map inputs; + final Map textInputs; + final Map fileInputs; - const _FlowRunDialog({required this.flow, required this.inputs}); + const _FlowRunDialog({ + required this.flow, + required this.textInputs, + required this.fileInputs, + }); @override State<_FlowRunDialog> createState() => _FlowRunDialogState(); @@ -264,7 +305,8 @@ class _FlowRunDialogState extends State<_FlowRunDialog> { super.initState(); _future = HubService.instance.runSavedFlow( name: widget.flow.name, - textInputs: widget.inputs, + textInputs: widget.textInputs, + fileInputs: widget.fileInputs, ); } diff --git a/lib/pages/welcome.dart b/lib/pages/welcome.dart index 6bf1449..43d91f7 100644 --- a/lib/pages/welcome.dart +++ b/lib/pages/welcome.dart @@ -819,8 +819,17 @@ class _DocReaderSheetState extends State<_DocReaderSheet> { final fallback = 'assets/docs/${widget.entry.slug}.md'; try { return await rootBundle.loadString(localised); - } catch (_) { - return rootBundle.loadString(fallback); + } catch (firstErr) { + try { + return await rootBundle.loadString(fallback); + } catch (secondErr) { + // Surface BOTH attempted paths and the underlying + // errors so the operator can copy a useful diagnostic + // out of FaiErrorBox without us having to ship a doc + // about reading Flutter asset errors. + throw 'tried "$localised": $firstErr\n' + 'then "$fallback": $secondErr'; + } } } @@ -887,11 +896,22 @@ class _DocReaderSheetState extends State<_DocReaderSheet> { if (snap.hasError) { return Padding( padding: const EdgeInsets.all(FaiSpace.xxl), - child: Text( - l.welcomeDocFailedToLoad(snap.error.toString()), - style: theme.textTheme.bodyMedium?.copyWith( - color: theme.colorScheme.error, - ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + l.welcomeDocFailedToLoad(''), + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.error, + ), + ), + const SizedBox(height: FaiSpace.sm), + FaiErrorBox( + text: snap.error.toString(), + isError: true, + maxHeight: 200, + ), + ], ), ); } diff --git a/pubspec.yaml b/pubspec.yaml index 115ced2..a004f60 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.39.1 +version: 0.40.0 environment: sdk: ^3.11.0-200.1.beta