feat: add frontend app, improve static file serving with lazy loading and root redirect
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'models.dart';
|
||||
|
||||
class APIError implements Exception {
|
||||
final int statusCode;
|
||||
final String message;
|
||||
APIError(this.statusCode, this.message);
|
||||
@override
|
||||
String toString() => 'APIError $statusCode: $message';
|
||||
}
|
||||
|
||||
class APIClient {
|
||||
final String baseUrl;
|
||||
|
||||
APIClient({this.baseUrl = 'http://localhost:8000'});
|
||||
|
||||
String _url(String path) => '$baseUrl$path';
|
||||
|
||||
static dynamic _decodeBody(http.Response response) {
|
||||
return jsonDecode(response.body);
|
||||
}
|
||||
|
||||
static Map<String, dynamic>? _asMap(dynamic d) {
|
||||
if (d is Map<String, dynamic>) return d;
|
||||
if (d is Map) return d.cast<String, dynamic>();
|
||||
return null;
|
||||
}
|
||||
|
||||
static void _checkError(http.Response response) {
|
||||
if (response.statusCode >= 400) {
|
||||
final body = _decodeBody(response);
|
||||
final map = _asMap(body) ?? {};
|
||||
throw APIError(
|
||||
response.statusCode,
|
||||
map['error'] as String? ?? 'Request failed',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<Pipeline>> listPipelines() async {
|
||||
final res = await http.get(Uri.parse(_url('/api/pipelines')));
|
||||
_checkError(res);
|
||||
final data = _decodeBody(res);
|
||||
final arr = data is List ? data : [];
|
||||
final result = <Pipeline>[];
|
||||
for (final e in arr) {
|
||||
final m = _asMap(e);
|
||||
if (m != null) result.add(Pipeline.fromJson(m));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<Pipeline> getPipeline(String id) async {
|
||||
final res = await http.get(Uri.parse(_url('/api/pipelines/$id')));
|
||||
_checkError(res);
|
||||
final data = _decodeBody(res);
|
||||
final map = _asMap(data) ?? {};
|
||||
final inner = _asMap(map['data']) ?? map;
|
||||
return Pipeline.fromJson(inner);
|
||||
}
|
||||
|
||||
Future<Pipeline> createPipeline(Pipeline pipeline) async {
|
||||
final res = await http.post(
|
||||
Uri.parse(_url('/api/pipelines')),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: jsonEncode(pipeline.toJson()),
|
||||
);
|
||||
_checkError(res);
|
||||
final data = _decodeBody(res);
|
||||
final map = _asMap(data) ?? {};
|
||||
final inner = _asMap(map['data']) ?? map;
|
||||
return Pipeline.fromJson(inner);
|
||||
}
|
||||
|
||||
Future<void> deletePipeline(String id) async {
|
||||
final res = await http.delete(Uri.parse(_url('/api/pipelines/$id')));
|
||||
_checkError(res);
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> startRun(String pipelineId) async {
|
||||
final res = await http.post(
|
||||
Uri.parse(_url('/api/runs')),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: jsonEncode({'pipeline_id': pipelineId}),
|
||||
);
|
||||
_checkError(res);
|
||||
final data = _decodeBody(res);
|
||||
final map = _asMap(data) ?? {};
|
||||
return _asMap(map['data']) ?? map;
|
||||
}
|
||||
|
||||
Future<RunState> getRunStatus(String runId) async {
|
||||
final res = await http.get(Uri.parse(_url('/api/runs/$runId/status')));
|
||||
_checkError(res);
|
||||
final data = _decodeBody(res);
|
||||
final map = _asMap(data) ?? {};
|
||||
final inner = _asMap(map['data']) ?? map;
|
||||
return RunState.fromJson(inner);
|
||||
}
|
||||
|
||||
Future<String> getRunLog(String runId) async {
|
||||
final res = await http.get(Uri.parse(_url('/api/runs/$runId/log')));
|
||||
_checkError(res);
|
||||
return res.body;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
class Prompt {
|
||||
final String id;
|
||||
final String text;
|
||||
final String title;
|
||||
final int order;
|
||||
|
||||
Prompt({
|
||||
required this.id,
|
||||
required this.text,
|
||||
required this.title,
|
||||
required this.order,
|
||||
});
|
||||
|
||||
factory Prompt.fromJson(Map<String, dynamic> json) {
|
||||
return Prompt(
|
||||
id: json['id'] as String? ?? '',
|
||||
text: json['text'] as String? ?? '',
|
||||
title: json['title'] as String? ?? '',
|
||||
order: json['order'] as int? ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {'id': id, 'text': text, 'title': title, 'order': order};
|
||||
}
|
||||
|
||||
Prompt copyWith({String? id, String? text, String? title, int? order}) {
|
||||
return Prompt(
|
||||
id: id ?? this.id,
|
||||
text: text ?? this.text,
|
||||
title: title ?? this.title,
|
||||
order: order ?? this.order,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Pipeline {
|
||||
final String id;
|
||||
final String name;
|
||||
final List<Prompt> prompts;
|
||||
final String workingDir;
|
||||
final String createdAt;
|
||||
final String updatedAt;
|
||||
|
||||
Pipeline({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.prompts,
|
||||
required this.workingDir,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
});
|
||||
|
||||
factory Pipeline.fromJson(Map<String, dynamic> json) {
|
||||
return Pipeline(
|
||||
id: json['id'] as String? ?? '',
|
||||
name: json['name'] as String? ?? '',
|
||||
prompts:
|
||||
(json['prompts'] as List<dynamic>?)
|
||||
?.map((p) => Prompt.fromJson(p as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
[],
|
||||
workingDir: json['working_dir'] as String? ?? '',
|
||||
createdAt: json['created_at'] as String? ?? '',
|
||||
updatedAt: json['updated_at'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'prompts': prompts.map((p) => p.toJson()).toList(),
|
||||
'working_dir': workingDir,
|
||||
'created_at': createdAt,
|
||||
'updated_at': updatedAt,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
enum RunStatus { pending, running, completed, error }
|
||||
|
||||
RunStatus parseRunStatus(String s) {
|
||||
switch (s) {
|
||||
case 'pending':
|
||||
return RunStatus.pending;
|
||||
case 'running':
|
||||
return RunStatus.running;
|
||||
case 'completed':
|
||||
return RunStatus.completed;
|
||||
case 'error':
|
||||
return RunStatus.error;
|
||||
default:
|
||||
return RunStatus.pending;
|
||||
}
|
||||
}
|
||||
|
||||
class StepResult {
|
||||
final int stepIndex;
|
||||
final String title;
|
||||
final RunStatus status;
|
||||
final int returnCode;
|
||||
final String output;
|
||||
final String error;
|
||||
|
||||
StepResult({
|
||||
required this.stepIndex,
|
||||
required this.title,
|
||||
required this.status,
|
||||
required this.returnCode,
|
||||
required this.output,
|
||||
required this.error,
|
||||
});
|
||||
|
||||
factory StepResult.fromJson(Map<String, dynamic> json) {
|
||||
return StepResult(
|
||||
stepIndex: json['step_index'] as int? ?? 0,
|
||||
title: json['title'] as String? ?? '',
|
||||
status: parseRunStatus(json['status'] as String? ?? 'pending'),
|
||||
returnCode: json['returncode'] as int? ?? 0,
|
||||
output: json['output'] as String? ?? '',
|
||||
error: json['error'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RunState {
|
||||
final String runId;
|
||||
final String pipelineId;
|
||||
final RunStatus status;
|
||||
final int currentStep;
|
||||
final List<StepResult> steps;
|
||||
|
||||
RunState({
|
||||
required this.runId,
|
||||
required this.pipelineId,
|
||||
required this.status,
|
||||
required this.currentStep,
|
||||
required this.steps,
|
||||
});
|
||||
|
||||
factory RunState.fromJson(Map<String, dynamic> json) {
|
||||
return RunState(
|
||||
runId: json['run_id'] as String? ?? '',
|
||||
pipelineId: json['pipeline_id'] as String? ?? '',
|
||||
status: parseRunStatus(json['status'] as String? ?? 'pending'),
|
||||
currentStep: json['current_step'] as int? ?? -1,
|
||||
steps:
|
||||
(json['steps'] as List<dynamic>?)
|
||||
?.map((s) => StepResult.fromJson(s as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
[],
|
||||
);
|
||||
}
|
||||
|
||||
bool get isFinished =>
|
||||
status == RunStatus.completed || status == RunStatus.error;
|
||||
}
|
||||
Reference in New Issue
Block a user