feat(studio): @-syntax file inputs in flows, copyable doc errors, services-row overflow (v0.40.0)
Three concrete fixes against today's user feedback.
- Flows that take binary inputs (extract / extract-summarize /
…) work from the Flows tab. The run-flow input dialog now
accepts the same `@/path/to/file` syntax `fai run --input`
uses on the CLI: any value beginning with `@` is read as
bytes and sent as a binary Payload; plain values still flow
through as text. The dialog hint copy and the example
placeholder reflect the new syntax. File-read failures
surface as a SnackBar before the run dialog opens, so a
typo in the path doesn't reach the hub. Threaded through
`_FlowRunDialog` and `HubService.runSavedFlow`, which both
carry separate `textInputs` and `fileInputs` maps now and
forward to the SDK's mixed-mode runSavedFlow (v0.14.0).
- The Welcome doc-reader's error path uses `FaiErrorBox` so
the actual underlying error is selectable + copy-to-
clipboard via the existing widget. Plus the loader throws
a richer error string that names *both* attempted asset
paths (`<slug>_<lang>.md` and the EN fallback) and the
underlying exception each, so the operator can paste a
diagnostic into a chat without us having to ship a
separate "how to read Flutter asset errors" doc.
- Doctor's empty Services panel had a horizontal RenderFlex
overflow at narrow widths because the long mono-spaced
hint ("add to ~/.fai/config.yaml under services:") and
the leading icon+text both demanded full intrinsic width
in a single Row. Now wraps via a `Wrap` widget so the
hint flows to a second line on narrow viewports and stays
in the same row when there's space.
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
parent
eb447c6ec0
commit
63e19974c0
11 changed files with 118 additions and 39 deletions
|
|
@ -5,6 +5,8 @@
|
||||||
// Methods return UI-friendly types so pages stay free of
|
// Methods return UI-friendly types so pages stay free of
|
||||||
// protobuf imports.
|
// protobuf imports.
|
||||||
|
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:fai_dart_sdk/fai_dart_sdk.dart';
|
import 'package:fai_dart_sdk/fai_dart_sdk.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
@ -544,15 +546,20 @@ class HubService {
|
||||||
..sort((a, b) => a.name.compareTo(b.name));
|
..sort((a, b) => a.name.compareTo(b.name));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run a saved flow with the supplied text inputs. Returns
|
/// Run a saved flow with the supplied inputs. Text values
|
||||||
/// the named outputs as a map of UI-friendly strings.
|
/// 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<Map<String, String>> runSavedFlow({
|
Future<Map<String, String>> runSavedFlow({
|
||||||
required String name,
|
required String name,
|
||||||
required Map<String, String> textInputs,
|
Map<String, String> textInputs = const {},
|
||||||
|
Map<String, Uint8List> fileInputs = const {},
|
||||||
}) async {
|
}) async {
|
||||||
final r = await _client.runSavedFlow(
|
final r = await _client.runSavedFlow(
|
||||||
name: name,
|
name: name,
|
||||||
textInputs: textInputs,
|
textInputs: textInputs,
|
||||||
|
fileInputs: fileInputs,
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
for (final entry in r.outputs.entries)
|
for (final entry in r.outputs.entries)
|
||||||
|
|
|
||||||
|
|
@ -389,8 +389,8 @@
|
||||||
"flowsNoneHint": "Flow speichern mit `fai admin flows save <name> <flow.yaml>`, dann taucht er hier auf.",
|
"flowsNoneHint": "Flow speichern mit `fai admin flows save <name> <flow.yaml>`, dann taucht er hier auf.",
|
||||||
"flowsRunDialogTitle": "{name} ausführen",
|
"flowsRunDialogTitle": "{name} ausführen",
|
||||||
"@flowsRunDialogTitle": { "placeholders": { "name": { "type": "String" } } },
|
"@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.",
|
"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=World\ntarget_language=English\nsummary_style=three bullet points",
|
"flowsInputsHint": "name=Welt\ndocument=@/Users/ich/Dokumente/beispiel.pdf\nstil=drei Stichpunkte",
|
||||||
"flowsRunButton": "Starten",
|
"flowsRunButton": "Starten",
|
||||||
"flowsRunningTitle": "{name} läuft",
|
"flowsRunningTitle": "{name} läuft",
|
||||||
"@flowsRunningTitle": { "placeholders": { "name": { "type": "String" } } },
|
"@flowsRunningTitle": { "placeholders": { "name": { "type": "String" } } },
|
||||||
|
|
|
||||||
|
|
@ -390,8 +390,8 @@
|
||||||
"flowsNoneHint": "Save a flow with `fai admin flows save <name> <flow.yaml>` and it shows up here.",
|
"flowsNoneHint": "Save a flow with `fai admin flows save <name> <flow.yaml>` and it shows up here.",
|
||||||
"flowsRunDialogTitle": "Run {name}",
|
"flowsRunDialogTitle": "Run {name}",
|
||||||
"@flowsRunDialogTitle": { "placeholders": { "name": { "type": "String" } } },
|
"@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.",
|
"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\ntarget_language=English\nsummary_style=three bullet points",
|
"flowsInputsHint": "name=World\ndocument=@/Users/me/Documents/sample.pdf\nstyle=three bullet points",
|
||||||
"flowsRunButton": "Run",
|
"flowsRunButton": "Run",
|
||||||
"flowsRunningTitle": "Running {name}",
|
"flowsRunningTitle": "Running {name}",
|
||||||
"@flowsRunningTitle": { "placeholders": { "name": { "type": "String" } } },
|
"@flowsRunningTitle": { "placeholders": { "name": { "type": "String" } } },
|
||||||
|
|
|
||||||
|
|
@ -1823,13 +1823,13 @@ abstract class AppLocalizations {
|
||||||
/// No description provided for @flowsRunDialogBody.
|
/// No description provided for @flowsRunDialogBody.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// 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;
|
String get flowsRunDialogBody;
|
||||||
|
|
||||||
/// No description provided for @flowsInputsHint.
|
/// No description provided for @flowsInputsHint.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// 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;
|
String get flowsInputsHint;
|
||||||
|
|
||||||
/// No description provided for @flowsRunButton.
|
/// No description provided for @flowsRunButton.
|
||||||
|
|
|
||||||
|
|
@ -1025,11 +1025,11 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get flowsRunDialogBody =>
|
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
|
@override
|
||||||
String get flowsInputsHint =>
|
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
|
@override
|
||||||
String get flowsRunButton => 'Starten';
|
String get flowsRunButton => 'Starten';
|
||||||
|
|
|
||||||
|
|
@ -1040,11 +1040,11 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get flowsRunDialogBody =>
|
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
|
@override
|
||||||
String get flowsInputsHint =>
|
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
|
@override
|
||||||
String get flowsRunButton => 'Run';
|
String get flowsRunButton => 'Run';
|
||||||
|
|
|
||||||
|
|
@ -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.39.1';
|
const String kStudioVersion = '0.40.0';
|
||||||
|
|
||||||
Future<void> main() async {
|
Future<void> main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
|
||||||
|
|
@ -734,8 +734,17 @@ class _ServicesPanel extends StatelessWidget {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final l = AppLocalizations.of(context)!;
|
final l = AppLocalizations.of(context)!;
|
||||||
if (snapshot.services.isEmpty) {
|
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(
|
return FaiCard(
|
||||||
child: Row(
|
child: Wrap(
|
||||||
|
spacing: FaiSpace.sm,
|
||||||
|
runSpacing: FaiSpace.xs,
|
||||||
|
crossAxisAlignment: WrapCrossAlignment.center,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(
|
||||||
Icons.dns_outlined,
|
Icons.dns_outlined,
|
||||||
|
|
@ -749,7 +758,8 @@ class _ServicesPanel extends StatelessWidget {
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Spacer(),
|
],
|
||||||
|
),
|
||||||
Text(
|
Text(
|
||||||
l.doctorServicesEmptyHint,
|
l.doctorServicesEmptyHint,
|
||||||
style: FaiTheme.mono(
|
style: FaiTheme.mono(
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
import 'dart:io';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../data/hub.dart';
|
import '../data/hub.dart';
|
||||||
|
|
@ -29,11 +32,44 @@ class _FlowsPageState extends State<FlowsPage> {
|
||||||
Future<void> _runFlow(SavedFlow flow) async {
|
Future<void> _runFlow(SavedFlow flow) async {
|
||||||
final inputs = await _FlowInputDialog.show(context, flow);
|
final inputs = await _FlowInputDialog.show(context, flow);
|
||||||
if (inputs == null) return;
|
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 = <String, String>{};
|
||||||
|
final fileInputs = <String, Uint8List>{};
|
||||||
|
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 (!mounted) return;
|
||||||
|
if (readError != null) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(readError)),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
showDialog<void>(
|
showDialog<void>(
|
||||||
context: context,
|
context: context,
|
||||||
barrierDismissible: false,
|
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 {
|
class _FlowRunDialog extends StatefulWidget {
|
||||||
final SavedFlow flow;
|
final SavedFlow flow;
|
||||||
final Map<String, String> inputs;
|
final Map<String, String> textInputs;
|
||||||
|
final Map<String, Uint8List> fileInputs;
|
||||||
|
|
||||||
const _FlowRunDialog({required this.flow, required this.inputs});
|
const _FlowRunDialog({
|
||||||
|
required this.flow,
|
||||||
|
required this.textInputs,
|
||||||
|
required this.fileInputs,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_FlowRunDialog> createState() => _FlowRunDialogState();
|
State<_FlowRunDialog> createState() => _FlowRunDialogState();
|
||||||
|
|
@ -264,7 +305,8 @@ class _FlowRunDialogState extends State<_FlowRunDialog> {
|
||||||
super.initState();
|
super.initState();
|
||||||
_future = HubService.instance.runSavedFlow(
|
_future = HubService.instance.runSavedFlow(
|
||||||
name: widget.flow.name,
|
name: widget.flow.name,
|
||||||
textInputs: widget.inputs,
|
textInputs: widget.textInputs,
|
||||||
|
fileInputs: widget.fileInputs,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -819,8 +819,17 @@ class _DocReaderSheetState extends State<_DocReaderSheet> {
|
||||||
final fallback = 'assets/docs/${widget.entry.slug}.md';
|
final fallback = 'assets/docs/${widget.entry.slug}.md';
|
||||||
try {
|
try {
|
||||||
return await rootBundle.loadString(localised);
|
return await rootBundle.loadString(localised);
|
||||||
} catch (_) {
|
} catch (firstErr) {
|
||||||
return rootBundle.loadString(fallback);
|
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,12 +896,23 @@ class _DocReaderSheetState extends State<_DocReaderSheet> {
|
||||||
if (snap.hasError) {
|
if (snap.hasError) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.all(FaiSpace.xxl),
|
padding: const EdgeInsets.all(FaiSpace.xxl),
|
||||||
child: Text(
|
child: Column(
|
||||||
l.welcomeDocFailedToLoad(snap.error.toString()),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
l.welcomeDocFailedToLoad(''),
|
||||||
style: theme.textTheme.bodyMedium?.copyWith(
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
color: theme.colorScheme.error,
|
color: theme.colorScheme.error,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: FaiSpace.sm),
|
||||||
|
FaiErrorBox(
|
||||||
|
text: snap.error.toString(),
|
||||||
|
isError: true,
|
||||||
|
maxHeight: 200,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return Markdown(
|
return Markdown(
|
||||||
|
|
|
||||||
|
|
@ -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.39.1
|
version: 0.40.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