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
100
lib/src/run_driver.dart
Normal file
100
lib/src/run_driver.dart
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
// Host-supplied bridge between the editor and the hub.
|
||||
//
|
||||
// The editor package can't talk to the hub directly — that's
|
||||
// the host's job (Studio uses the gRPC SDK, a CLI host could
|
||||
// shell out to `fai run`, a test harness can stub everything
|
||||
// in-memory). The bridge keeps the editor reusable across
|
||||
// any host that can implement the two methods below.
|
||||
//
|
||||
// Lifecycle expected by the editor:
|
||||
//
|
||||
// 1. Subscribe to `events()` BEFORE calling `runFlow()`.
|
||||
// The hub starts emitting `step.started` events the
|
||||
// instant the flow begins; the host's subscription must
|
||||
// already be live so the first one doesn't get lost.
|
||||
// 2. Call `runFlow()` with the flow name + collected inputs.
|
||||
// The returned Future resolves with the final outputs
|
||||
// (or throws on failure).
|
||||
// 3. Unsubscribe from `events()` when the run resolves.
|
||||
|
||||
import 'dart:typed_data';
|
||||
|
||||
abstract class FlowRunDriver {
|
||||
/// Trigger a run of [flowName] with the supplied inputs.
|
||||
/// Text + file inputs come in separate maps so the host
|
||||
/// can wire them to the right payload shape. Returns the
|
||||
/// flow's final outputs map keyed by output name.
|
||||
Future<Map<String, FlowOutputValue>> runFlow({
|
||||
required String flowName,
|
||||
required Map<String, String> textInputs,
|
||||
required Map<String, Uint8List> fileInputs,
|
||||
required Map<String, String> fileMimes,
|
||||
});
|
||||
|
||||
/// Live event stream scoped to whatever the host can
|
||||
/// observe — typically the hub's `StreamEvents` RPC
|
||||
/// filtered to step.* event types for the running flow.
|
||||
/// May arrive late (subscribe BEFORE calling runFlow!) and
|
||||
/// may emit events for unrelated runs; the editor filters
|
||||
/// by [FlowRunEvent.flowName] before applying.
|
||||
Stream<FlowRunEvent> events();
|
||||
}
|
||||
|
||||
/// What the editor needs out of a single event tick. Maps
|
||||
/// 1:1 to the hub's `step.*` events but with the typing
|
||||
/// projected into the editor's vocabulary so we don't carry
|
||||
/// the SDK's proto types across the package boundary.
|
||||
sealed class FlowRunEvent {
|
||||
final String flowName;
|
||||
final String stepId;
|
||||
const FlowRunEvent({required this.flowName, required this.stepId});
|
||||
}
|
||||
|
||||
class StepStarted extends FlowRunEvent {
|
||||
const StepStarted({required super.flowName, required super.stepId});
|
||||
}
|
||||
|
||||
class StepCompleted extends FlowRunEvent {
|
||||
final int durationMs;
|
||||
const StepCompleted({
|
||||
required super.flowName,
|
||||
required super.stepId,
|
||||
required this.durationMs,
|
||||
});
|
||||
}
|
||||
|
||||
class StepFailed extends FlowRunEvent {
|
||||
final String error;
|
||||
const StepFailed({
|
||||
required super.flowName,
|
||||
required super.stepId,
|
||||
required this.error,
|
||||
});
|
||||
}
|
||||
|
||||
class StepAwaitingApproval extends FlowRunEvent {
|
||||
const StepAwaitingApproval({required super.flowName, required super.stepId});
|
||||
}
|
||||
|
||||
/// One output value, after the run completed. The editor
|
||||
/// renders text inline, JSON pretty-printed, bytes as a
|
||||
/// download badge.
|
||||
sealed class FlowOutputValue {
|
||||
const FlowOutputValue();
|
||||
}
|
||||
|
||||
class FlowOutputText extends FlowOutputValue {
|
||||
final String value;
|
||||
const FlowOutputText(this.value);
|
||||
}
|
||||
|
||||
class FlowOutputJson extends FlowOutputValue {
|
||||
final Object? value;
|
||||
const FlowOutputJson(this.value);
|
||||
}
|
||||
|
||||
class FlowOutputBytes extends FlowOutputValue {
|
||||
final Uint8List value;
|
||||
final String mimeType;
|
||||
const FlowOutputBytes(this.value, this.mimeType);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue