feat(editor): three-tab WYSIWYG editor — graph / text / run
Full rewrite of the editor surface, layered on top of the
FlowGraph foundation. One in-memory flow drives three tabs
that the operator can flip between freely:
- Graph: a drag-and-drop canvas. Nodes are step cards with
port dots on their left (one per `with:` field) and a
combined output port on the right. Pinned inputs and
outputs pseudo-nodes sit at the left and right edges so
every flow has a visually obvious source and sink. Pan +
zoom via InteractiveViewer; drag a node by its body to
reposition it (positions persisted to a sidecar JSON file
under ~/.fai/data/flows/.layout/<name>.json — kept OUT of
the YAML so `fai run` stays byte-stable).
- Text: the existing YAML CodeField with expands:true so
line 1 anchors at the top edge. YAML-aware syntax
highlighting picks up the theme's primary / secondary /
tertiary palette for keys / strings / numbers.
- Run: an inputs form (text fields + file-pick), a Start
button that calls the host's FlowRunDriver, a live step
list driven by the driver's event stream (matches the
`fai run` CLI rendering — ◻ pending, · running, ✔ done +
duration, ✗ failed, ⏸ awaiting approval), and the typed
outputs once the run resolves.
Source of truth = the YAML text. Graph edits emit fresh YAML
into the shared CodeController; text edits re-parse the
graph on a 350 ms debounce. Layout sidecar persists drag
positions only.
New public API (lib/fai_studio_flow_editor.dart):
FlowEditorPage(
initialFlowName: ...,
locale: ...,
runDriver: FlowRunDriver?, // NEW — host bridge
availableCapabilities: List<String>, // NEW — for the
// capability picker
// dialog when adding
// a step
)
The host (Studio) implements FlowRunDriver to bridge the
hub's gRPC SDK into the editor's event vocabulary. The
StepStarted/Completed/Failed/AwaitingApproval events are
shared verbatim with the CLI's run_progress renderer so
both surfaces speak the same visual language.
Files in this commit:
- lib/src/editor_controller.dart — shared state +
debounced reparse loop
- lib/src/run_driver.dart — host bridge
interface + event types
- lib/src/widgets/flow_canvas.dart — pan / zoom / drag /
port-to-port connection drawing
- lib/src/widgets/flow_node.dart — node card primitive
(module / approval / inputs / outputs variants)
- lib/src/widgets/edge_painter.dart — single CustomPainter
for every edge + draft drag line, cubic bezier with
arrow-head caps
- lib/src/widgets/properties_panel.dart — right-side editor
when a step is selected (rename id, change capability, add
/ remove / rename with-fields, delete step)
- lib/src/widgets/capability_picker.dart — searchable list
dialog used by Add-step
- lib/src/widgets/run_tab.dart — inputs form +
live step progress + outputs renderer
- lib/src/flow_editor_page.dart — host scaffolding,
toolbar, file list, three-tab body, keyboard shortcuts
- lib/src/l10n.dart — EN + DE strings for
every new label
- lib/fai_studio_flow_editor.dart — exports the new
public types (FlowRunDriver, FlowRunEvent variants,
FlowOutputValue variants)
flutter analyze: 0 issues. flutter test: 7/7 green.
Signed-off-by: flemming-it <sf@flemming.it>
This commit is contained in:
parent
dbd9a0004f
commit
870cbc29f7
12 changed files with 2769 additions and 408 deletions
103
lib/src/widgets/capability_picker.dart
Normal file
103
lib/src/widgets/capability_picker.dart
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
// Capability picker — modal dialog for choosing which
|
||||
// capability a new step should use. Studio provides the
|
||||
// list (it lives on the hub); the picker is a thin
|
||||
// searchable list on top.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../l10n.dart';
|
||||
import '../tokens.dart';
|
||||
|
||||
class CapabilityPicker extends StatefulWidget {
|
||||
final List<String> capabilities;
|
||||
final FlowEditorStrings strings;
|
||||
const CapabilityPicker({
|
||||
super.key,
|
||||
required this.capabilities,
|
||||
required this.strings,
|
||||
});
|
||||
|
||||
static Future<String?> show(
|
||||
BuildContext context, {
|
||||
required List<String> capabilities,
|
||||
required FlowEditorStrings strings,
|
||||
}) {
|
||||
return showDialog<String>(
|
||||
context: context,
|
||||
builder: (_) =>
|
||||
CapabilityPicker(capabilities: capabilities, strings: strings),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
State<CapabilityPicker> createState() => _CapabilityPickerState();
|
||||
}
|
||||
|
||||
class _CapabilityPickerState extends State<CapabilityPicker> {
|
||||
String _query = '';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final filtered = _query.isEmpty
|
||||
? widget.capabilities
|
||||
: widget.capabilities
|
||||
.where((c) => c.toLowerCase().contains(_query.toLowerCase()))
|
||||
.toList();
|
||||
return Dialog(
|
||||
child: SizedBox(
|
||||
width: 480,
|
||||
height: 520,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(FaiSpace.md),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
widget.strings.pickerTitle,
|
||||
style: theme.textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: FaiSpace.sm),
|
||||
TextField(
|
||||
autofocus: true,
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: const Icon(Icons.search, size: 18),
|
||||
hintText: widget.strings.pickerSearch,
|
||||
isDense: true,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
onChanged: (v) => setState(() => _query = v),
|
||||
),
|
||||
const SizedBox(height: FaiSpace.sm),
|
||||
Expanded(
|
||||
child: filtered.isEmpty
|
||||
? Center(
|
||||
child: Text(
|
||||
widget.strings.pickerEmpty,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
)
|
||||
: ListView.builder(
|
||||
itemCount: filtered.length,
|
||||
itemBuilder: (_, i) {
|
||||
final cap = filtered[i];
|
||||
return ListTile(
|
||||
dense: true,
|
||||
title: Text(
|
||||
cap,
|
||||
style: const TextStyle(fontFamily: 'monospace'),
|
||||
),
|
||||
onTap: () => Navigator.pop(context, cap),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue