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
105
lib/src/widgets/edge_painter.dart
Normal file
105
lib/src/widgets/edge_painter.dart
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
// EdgePainter — draws every connection on the canvas as a
|
||||
// cubic-bezier curve. Pure render layer: receives a flat list
|
||||
// of port-to-port positions (already resolved against node
|
||||
// positions) and paints them in one go.
|
||||
//
|
||||
// Connections are painted in a single CustomPaint so we don't
|
||||
// pay the widget-tree cost of one painter per edge. Hit
|
||||
// testing is delegated to port hit boxes drawn over the
|
||||
// canvas; the painter itself is wrapped in IgnorePointer in
|
||||
// the canvas so nothing here eats taps meant for nodes.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class EdgeSegment {
|
||||
final Offset from;
|
||||
final Offset to;
|
||||
final EdgeAccent accent;
|
||||
const EdgeSegment({
|
||||
required this.from,
|
||||
required this.to,
|
||||
this.accent = EdgeAccent.normal,
|
||||
});
|
||||
}
|
||||
|
||||
enum EdgeAccent { normal, highlight, draftDrag }
|
||||
|
||||
class EdgePainter extends CustomPainter {
|
||||
final List<EdgeSegment> segments;
|
||||
final Color baseColor;
|
||||
final Color highlightColor;
|
||||
final Color draftColor;
|
||||
|
||||
EdgePainter({
|
||||
required this.segments,
|
||||
required this.baseColor,
|
||||
required this.highlightColor,
|
||||
required this.draftColor,
|
||||
});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
for (final seg in segments) {
|
||||
final color = switch (seg.accent) {
|
||||
EdgeAccent.normal => baseColor,
|
||||
EdgeAccent.highlight => highlightColor,
|
||||
EdgeAccent.draftDrag => draftColor,
|
||||
};
|
||||
final paint = Paint()
|
||||
..color = color
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = seg.accent == EdgeAccent.highlight ? 2.5 : 1.8
|
||||
..strokeCap = StrokeCap.round;
|
||||
final path = _bezier(seg.from, seg.to);
|
||||
canvas.drawPath(path, paint);
|
||||
// End-cap arrow: small filled triangle pointing at the
|
||||
// target port so the reader can tell direction at a
|
||||
// glance without inspecting the geometry.
|
||||
_drawArrowHead(canvas, seg.to, seg.from, color);
|
||||
}
|
||||
}
|
||||
|
||||
Path _bezier(Offset from, Offset to) {
|
||||
// Cubic with horizontal handles — typical "flow diagram"
|
||||
// smooth curve. The handle length scales with the
|
||||
// horizontal distance so steep verticals still look
|
||||
// graceful.
|
||||
final dx = (to.dx - from.dx).abs();
|
||||
final handleLen = (dx / 2).clamp(40.0, 220.0);
|
||||
return Path()
|
||||
..moveTo(from.dx, from.dy)
|
||||
..cubicTo(
|
||||
from.dx + handleLen,
|
||||
from.dy,
|
||||
to.dx - handleLen,
|
||||
to.dy,
|
||||
to.dx,
|
||||
to.dy,
|
||||
);
|
||||
}
|
||||
|
||||
void _drawArrowHead(Canvas canvas, Offset tip, Offset from, Color color) {
|
||||
// Approximate the incoming direction by sampling a
|
||||
// little before the tip. Good enough since the curve is
|
||||
// nearly horizontal near the end.
|
||||
final dir = tip.dx - from.dx;
|
||||
final orient = dir >= 0 ? 1.0 : -1.0;
|
||||
const size = 6.0;
|
||||
final p1 = tip;
|
||||
final p2 = Offset(tip.dx - size * orient, tip.dy - size * 0.7);
|
||||
final p3 = Offset(tip.dx - size * orient, tip.dy + size * 0.7);
|
||||
final path = Path()
|
||||
..moveTo(p1.dx, p1.dy)
|
||||
..lineTo(p2.dx, p2.dy)
|
||||
..lineTo(p3.dx, p3.dy)
|
||||
..close();
|
||||
canvas.drawPath(path, Paint()..color = color);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(EdgePainter old) =>
|
||||
old.segments != segments ||
|
||||
old.baseColor != baseColor ||
|
||||
old.highlightColor != highlightColor ||
|
||||
old.draftColor != draftColor;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue