feat: add frontend app, improve static file serving with lazy loading and root redirect
This commit is contained in:
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user