// StudioFlowRunDriver — adapter that bridges the editor // package's FlowRunDriver interface to Studio's HubService. // // Lives in Studio (not in the editor package) so the package // stays host-agnostic. The driver maps: // // - editor.runFlow(...) -> HubService.runSavedFlow // - editor.events() -> HubService.streamEvents filtered // to step.* events and reshaped // into the editor's FlowRunEvent // value classes. import 'dart:async'; import 'dart:typed_data'; import 'package:fai_studio_flow_editor/fai_studio_flow_editor.dart' as editor; import 'hub.dart'; class StudioFlowRunDriver implements editor.FlowRunDriver { StudioFlowRunDriver(); @override Future> runFlow({ required String flowName, required Map textInputs, required Map fileInputs, required Map fileMimes, }) async { final outputs = await HubService.instance.runSavedFlow( name: flowName, textInputs: textInputs, fileInputs: fileInputs, fileMimeTypes: fileMimes, ); final mapped = {}; for (final entry in outputs.entries) { mapped[entry.key] = _convertOutput(entry.value); } return mapped; } @override Stream events() { return HubService.instance .streamEvents( backfill: 0, types: const [ 'step.started', 'step.completed', 'step.failed', 'step.awaiting_approval', 'step.approved', 'step.rejected', ], ) .map(_convertEvent) .where((e) => e != null) .cast(); } editor.FlowRunEvent? _convertEvent(AuditEvent e) { final flowName = e.flowName; final stepId = e.stepId; if (flowName == null || flowName.isEmpty) return null; if (stepId == null || stepId.isEmpty) return null; switch (e.type) { case 'step.started': case 'step.approved': return editor.StepStarted(flowName: flowName, stepId: stepId); case 'step.completed': return editor.StepCompleted( flowName: flowName, stepId: stepId, durationMs: e.durationMs ?? 0, ); case 'step.failed': case 'step.rejected': return editor.StepFailed( flowName: flowName, stepId: stepId, error: e.error ?? '', ); case 'step.awaiting_approval': return editor.StepAwaitingApproval( flowName: flowName, stepId: stepId, ); } return null; } /// Studio's FlowOutput hierarchy is gRPC-shaped (one class /// per payload variant); the editor's is host-agnostic /// (text / json / bytes only). File outputs degrade to /// JSON containing the URI string — the editor's run-tab /// renderer is read-only so the operator can still see /// where the file went; Studio's richer FaiFlowOutput /// widget keeps file-open affordances available elsewhere. editor.FlowOutputValue _convertOutput(FlowOutput out) { return switch (out) { FlowOutputText() => editor.FlowOutputText(out.text), FlowOutputJson() => editor.FlowOutputJson(out.pretty), FlowOutputBytes() => editor.FlowOutputBytes(out.bytes, out.mimeType), FlowOutputFile() => editor.FlowOutputJson({ 'file_uri': out.uri, 'mime_type': out.mimeType, }), FlowOutputUnknown() => const editor.FlowOutputText(''), }; } }