feat: add frontend app, improve static file serving with lazy loading and root redirect

This commit is contained in:
2026-07-20 21:03:48 +03:00
parent 799e939037
commit d1e96d35aa
27 changed files with 2053 additions and 14 deletions
+107
View File
@@ -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;
}
}
+158
View File
@@ -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;
}