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
+49
View File
@@ -0,0 +1,49 @@
import 'package:flutter/material.dart';
import '../api/models.dart';
class PipelineCard extends StatelessWidget {
final Pipeline pipeline;
final VoidCallback onTap;
final VoidCallback onDelete;
const PipelineCard({
super.key,
required this.pipeline,
required this.onTap,
required this.onDelete,
});
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
child: ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
leading: const Icon(Icons.api, size: 32),
title: Text(
pipeline.name,
style: const TextStyle(fontWeight: FontWeight.bold),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${pipeline.prompts.length} prompt${pipeline.prompts.length != 1 ? 's' : ''}',
),
if (pipeline.workingDir.isNotEmpty)
Text(
'📁 ${pipeline.workingDir}',
style: const TextStyle(fontSize: 12, color: Colors.grey),
),
],
),
trailing: IconButton(
icon: const Icon(Icons.delete_outline, color: Colors.red),
onPressed: onDelete,
tooltip: 'Delete',
),
onTap: onTap,
),
);
}
}
+97
View File
@@ -0,0 +1,97 @@
import 'package:flutter/material.dart';
import '../api/models.dart';
class PromptEditor extends StatefulWidget {
final Prompt prompt;
final ValueChanged<Prompt> onChanged;
final VoidCallback? onDelete;
final bool showDelete;
const PromptEditor({
super.key,
required this.prompt,
required this.onChanged,
this.onDelete,
this.showDelete = true,
});
@override
State<PromptEditor> createState() => _PromptEditorState();
}
class _PromptEditorState extends State<PromptEditor> {
late TextEditingController textController;
late TextEditingController titleController;
int order = 0;
@override
void initState() {
super.initState();
textController = TextEditingController(text: widget.prompt.text);
titleController = TextEditingController(text: widget.prompt.title);
order = widget.prompt.order;
}
@override
void dispose() {
textController.dispose();
titleController.dispose();
super.dispose();
}
void _notifyChanged() {
widget.onChanged(
widget.prompt.copyWith(
text: textController.text,
title: titleController.text,
order: order,
),
);
}
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.only(bottom: 12),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Chip(label: Text('Step ${order + 1}')),
const Spacer(),
if (widget.showDelete && widget.onDelete != null)
IconButton(
icon: const Icon(Icons.close, size: 20),
onPressed: widget.onDelete,
tooltip: 'Remove prompt',
),
],
),
TextField(
controller: titleController,
decoration: const InputDecoration(
labelText: 'Title',
border: OutlineInputBorder(),
isDense: true,
),
onChanged: (_) => _notifyChanged(),
),
const SizedBox(height: 8),
TextField(
controller: textController,
decoration: const InputDecoration(
labelText: 'Prompt Text',
border: OutlineInputBorder(),
),
maxLines: 4,
onChanged: (_) => _notifyChanged(),
),
],
),
),
);
}
}
@@ -0,0 +1,47 @@
import 'package:flutter/material.dart';
import '../api/models.dart';
class RunStatusBadge extends StatelessWidget {
final RunStatus status;
const RunStatusBadge({super.key, required this.status});
Color _color(BuildContext context) {
switch (status) {
case RunStatus.pending:
return Colors.amber;
case RunStatus.running:
return Colors.blue;
case RunStatus.completed:
return Colors.green;
case RunStatus.error:
return Colors.red;
}
}
IconData _icon() {
switch (status) {
case RunStatus.pending:
return Icons.schedule;
case RunStatus.running:
return Icons.play_circle_outline;
case RunStatus.completed:
return Icons.check_circle;
case RunStatus.error:
return Icons.error;
}
}
@override
Widget build(BuildContext context) {
final c = _color(context);
return Chip(
avatar: Icon(_icon(), size: 16, color: c),
label: Text(
status.name.toUpperCase(),
style: TextStyle(color: c, fontWeight: FontWeight.bold, fontSize: 12),
),
backgroundColor: c.withValues(alpha: 0.1),
);
}
}
+82
View File
@@ -0,0 +1,82 @@
import 'package:flutter/material.dart';
import '../api/models.dart';
class StepProgress extends StatelessWidget {
final List<StepResult> steps;
final int totalSteps;
const StepProgress({
super.key,
required this.steps,
required this.totalSteps,
});
@override
Widget build(BuildContext context) {
if (totalSteps == 0) return const SizedBox.shrink();
return Column(
children: steps.map((step) {
IconData icon;
Color color;
switch (step.status) {
case RunStatus.pending:
icon = Icons.circle_outlined;
color = Colors.grey;
break;
case RunStatus.running:
icon = Icons.pending;
color = Colors.blue;
break;
case RunStatus.completed:
icon = Icons.check_circle;
color = Colors.green;
break;
case RunStatus.error:
icon = Icons.error;
color = Colors.red;
break;
}
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
children: [
Icon(icon, color: color, size: 24),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
step.title.isNotEmpty
? step.title
: 'Step ${step.stepIndex + 1}',
style: TextStyle(
fontWeight: FontWeight.bold,
color: step.status == RunStatus.running ? color : null,
),
),
if (step.status == RunStatus.error && step.error.isNotEmpty)
Text(
step.error,
style: const TextStyle(color: Colors.red, fontSize: 12),
),
],
),
),
Text(
step.status.name.toUpperCase(),
style: TextStyle(
fontSize: 11,
color: color,
fontWeight: FontWeight.bold,
),
),
],
),
);
}).toList(),
);
}
}