feat: add frontend app, improve static file serving with lazy loading and root redirect
This commit is contained in:
@@ -0,0 +1,254 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../api/models.dart';
|
||||
import '../widgets/prompt_editor.dart';
|
||||
import '../api/client.dart';
|
||||
|
||||
class PipelineEditorScreen extends StatefulWidget {
|
||||
final Pipeline? pipeline;
|
||||
|
||||
const PipelineEditorScreen({super.key, this.pipeline});
|
||||
|
||||
@override
|
||||
State<PipelineEditorScreen> createState() => _PipelineEditorScreenState();
|
||||
}
|
||||
|
||||
class _PipelineEditorScreenState extends State<PipelineEditorScreen> {
|
||||
late TextEditingController _idController;
|
||||
late TextEditingController _nameController;
|
||||
late TextEditingController _dirController;
|
||||
late List<Prompt> _prompts;
|
||||
bool _saving = false;
|
||||
String? _error;
|
||||
|
||||
late APIClient _api;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_api = APIClient(baseUrl: _parseBaseUrl());
|
||||
|
||||
if (widget.pipeline != null) {
|
||||
_idController = TextEditingController(text: widget.pipeline!.id);
|
||||
_nameController = TextEditingController(text: widget.pipeline!.name);
|
||||
_dirController = TextEditingController(text: widget.pipeline!.workingDir);
|
||||
_prompts = List.from(widget.pipeline!.prompts);
|
||||
} else {
|
||||
_idController = TextEditingController();
|
||||
_nameController = TextEditingController();
|
||||
_dirController = TextEditingController();
|
||||
_prompts = [];
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_idController.dispose();
|
||||
_nameController.dispose();
|
||||
_dirController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
static String _parseBaseUrl() {
|
||||
final uri = Uri.base;
|
||||
return '${uri.scheme}://${uri.host}${uri.port != 80 && uri.port != 443 ? ':${uri.port}' : ''}';
|
||||
}
|
||||
|
||||
void _addPrompt() {
|
||||
setState(() {
|
||||
_prompts.add(
|
||||
Prompt(id: _generateId(), text: '', title: '', order: _prompts.length),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void _removePrompt(int index) {
|
||||
setState(() {
|
||||
_prompts.removeAt(index);
|
||||
for (int i = index; i < _prompts.length; i++) {
|
||||
_prompts[i] = _prompts[i].copyWith(order: i);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _updatePrompt(int index, Prompt updated) {
|
||||
setState(() {
|
||||
_prompts[index] = updated;
|
||||
});
|
||||
}
|
||||
|
||||
static String _generateId() {
|
||||
return DateTime.now().millisecondsSinceEpoch.toRadixString(36);
|
||||
}
|
||||
|
||||
bool _validate() {
|
||||
if (_idController.text.trim().isEmpty) {
|
||||
setState(() => _error = 'ID is required');
|
||||
return false;
|
||||
}
|
||||
if (_nameController.text.trim().isEmpty) {
|
||||
setState(() => _error = 'Name is required');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<void> _save() async {
|
||||
if (!_validate()) return;
|
||||
|
||||
setState(() {
|
||||
_saving = true;
|
||||
_error = null;
|
||||
});
|
||||
|
||||
final sortedPrompts = List<Prompt>.from(_prompts)
|
||||
..sort((a, b) => a.order.compareTo(b.order));
|
||||
|
||||
final pipeline = Pipeline(
|
||||
id: _idController.text.trim(),
|
||||
name: _nameController.text.trim(),
|
||||
prompts: sortedPrompts,
|
||||
workingDir: _dirController.text.trim(),
|
||||
createdAt: widget.pipeline?.createdAt ?? '',
|
||||
updatedAt: '',
|
||||
);
|
||||
|
||||
try {
|
||||
final saved = await _api.createPipeline(pipeline);
|
||||
if (mounted) {
|
||||
Navigator.pop(context, saved);
|
||||
}
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_error = e.toString();
|
||||
_saving = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isEdit = widget.pipeline != null;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(isEdit ? 'Edit Pipeline' : 'Create Pipeline'),
|
||||
actions: [
|
||||
if (isEdit)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.check, color: Colors.green),
|
||||
onPressed: _saving ? null : _save,
|
||||
tooltip: 'Save',
|
||||
),
|
||||
],
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (_error != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: Card(
|
||||
color: Colors.red.shade50,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.error_outline, color: Colors.red),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: Text(_error!)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
TextField(
|
||||
controller: _idController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'ID',
|
||||
border: OutlineInputBorder(),
|
||||
isDense: true,
|
||||
),
|
||||
enabled: !isEdit,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: _nameController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Name',
|
||||
border: OutlineInputBorder(),
|
||||
isDense: true,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: _dirController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Working Directory',
|
||||
border: OutlineInputBorder(),
|
||||
isDense: true,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
'Prompts',
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||
),
|
||||
FilledButton.icon(
|
||||
onPressed: _addPrompt,
|
||||
icon: const Icon(Icons.add, size: 18),
|
||||
label: const Text('Add Step'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
if (_prompts.isEmpty)
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(20),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'No prompts yet. Add a step to get started.',
|
||||
style: TextStyle(color: Colors.grey),
|
||||
),
|
||||
),
|
||||
),
|
||||
for (int i = 0; i < _prompts.length; i++)
|
||||
PromptEditor(
|
||||
prompt: _prompts[i],
|
||||
showDelete: _prompts.length > 1,
|
||||
onChanged: (p) => _updatePrompt(i, p),
|
||||
onDelete: () => _removePrompt(i),
|
||||
),
|
||||
if (_prompts.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 16),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
height: 48,
|
||||
child: FilledButton.icon(
|
||||
onPressed: _saving ? null : _save,
|
||||
icon: _saving
|
||||
? const SizedBox(
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.save),
|
||||
label: Text(
|
||||
_saving
|
||||
? 'Saving...'
|
||||
: (isEdit ? 'Save Changes' : 'Create Pipeline'),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user