172 lines
5.9 KiB
Dart
172 lines
5.9 KiB
Dart
import '../../features/shared/job_files_repository.dart';
|
||
import '../../features/shared/tenant_team_repository.dart';
|
||
import '../../models/job_file.dart';
|
||
import '../../models/tenant.dart';
|
||
import '../api/pocketbase_client.dart';
|
||
|
||
// ── Message segments ──────────────────────────────────────────────────────────
|
||
|
||
sealed class MessageSegment {}
|
||
|
||
class TextSegment extends MessageSegment {
|
||
TextSegment(this.text);
|
||
final String text;
|
||
}
|
||
|
||
class ActionSegment extends MessageSegment {
|
||
ActionSegment(this.action);
|
||
final AiAction action;
|
||
}
|
||
|
||
// ── Action model ──────────────────────────────────────────────────────────────
|
||
|
||
class AiAction {
|
||
const AiAction({
|
||
required this.type,
|
||
required this.params,
|
||
required this.label,
|
||
});
|
||
final String type;
|
||
final Map<String, String> params;
|
||
final String label;
|
||
|
||
bool get isDangerous => type == 'cancel_job';
|
||
bool get isFileAction => type == 'job_files';
|
||
}
|
||
|
||
// ── Action outcome ────────────────────────────────────────────────────────────
|
||
|
||
sealed class ActionOutcome {}
|
||
|
||
class ActionSuccess extends ActionOutcome {
|
||
ActionSuccess(this.message);
|
||
final String message;
|
||
}
|
||
|
||
class ActionError extends ActionOutcome {
|
||
ActionError(this.error);
|
||
final String error;
|
||
}
|
||
|
||
class ActionFiles extends ActionOutcome {
|
||
ActionFiles(this.files);
|
||
final List<JobFile> files;
|
||
}
|
||
|
||
// ── Parser ────────────────────────────────────────────────────────────────────
|
||
|
||
List<MessageSegment> parseSegments(String text) {
|
||
// Strip code fences wrapping <dls-action> tags that the AI sometimes emits.
|
||
// Handles: ```xml\n<dls-action .../>\n``` and ```\n<dls-action .../>\n```
|
||
text = text.replaceAllMapped(
|
||
RegExp(r'```(?:xml)?\s*\n(\s*<dls-action\s[^>]*/>)\s*\n\s*```'),
|
||
(m) => m.group(1)!,
|
||
);
|
||
// Also handle inline variant: ```xml <dls-action .../> ```
|
||
text = text.replaceAllMapped(
|
||
RegExp(r'```(?:xml)?\s*(<dls-action\s[^>]*/>)\s*```'),
|
||
(m) => m.group(1)!,
|
||
);
|
||
|
||
final pattern = RegExp(r'<dls-action\s([^/]*?)/>', dotAll: true);
|
||
final segments = <MessageSegment>[];
|
||
int last = 0;
|
||
|
||
for (final m in pattern.allMatches(text)) {
|
||
final before = text.substring(last, m.start).trim();
|
||
if (before.isNotEmpty) segments.add(TextSegment(before));
|
||
|
||
final attrs = _parseAttrs(m.group(1) ?? '');
|
||
segments.add(ActionSegment(AiAction(
|
||
type: attrs['type'] ?? '',
|
||
params: attrs,
|
||
label: attrs['label'] ?? attrs['type'] ?? 'İşlem',
|
||
)));
|
||
last = m.end;
|
||
}
|
||
|
||
final rest = text.substring(last).trim();
|
||
if (rest.isNotEmpty) segments.add(TextSegment(rest));
|
||
return segments;
|
||
}
|
||
|
||
Map<String, String> _parseAttrs(String s) {
|
||
final result = <String, String>{};
|
||
for (final m in RegExp(r'(\w+)="([^"]*)"').allMatches(s)) {
|
||
result[m.group(1)!] = m.group(2)!;
|
||
}
|
||
return result;
|
||
}
|
||
|
||
// ── Executor ──────────────────────────────────────────────────────────────────
|
||
|
||
class AiActionExecutor {
|
||
static final _pb = PocketBaseClient.instance.pb;
|
||
|
||
static Future<ActionOutcome> execute(
|
||
AiAction action,
|
||
TenantMembership membership,
|
||
) async {
|
||
try {
|
||
return switch (action.type) {
|
||
'cancel_job' => await _cancelJob(action.params),
|
||
'mark_delivered' => await _markDelivered(action.params),
|
||
'job_files' => await _jobFiles(action.params),
|
||
'add_member' => await _addMember(action.params, membership),
|
||
_ => ActionError('Bilinmeyen işlem türü: ${action.type}'),
|
||
};
|
||
} catch (e) {
|
||
final msg = e.toString();
|
||
if (msg.length > 120) return ActionError('Sunucu hatası');
|
||
return ActionError(msg);
|
||
}
|
||
}
|
||
|
||
static Future<ActionOutcome> _cancelJob(Map<String, String> p) async {
|
||
final id = p['job_id'];
|
||
if (id == null || id.isEmpty) return ActionError('İş ID bulunamadı.');
|
||
await _pb.collection('jobs').update(id, body: {'status': 'cancelled'});
|
||
return ActionSuccess('İş başarıyla iptal edildi.');
|
||
}
|
||
|
||
static Future<ActionOutcome> _markDelivered(Map<String, String> p) async {
|
||
final id = p['job_id'];
|
||
if (id == null || id.isEmpty) return ActionError('İş ID bulunamadı.');
|
||
await _pb.collection('jobs').update(id, body: {'status': 'delivered'});
|
||
return ActionSuccess('İş teslim edildi olarak işaretlendi.');
|
||
}
|
||
|
||
static Future<ActionOutcome> _jobFiles(Map<String, String> p) async {
|
||
final id = p['job_id'];
|
||
if (id == null || id.isEmpty) return ActionError('İş ID bulunamadı.');
|
||
final files = await JobFilesRepository.instance.listForJob(id);
|
||
if (files.isEmpty) return ActionSuccess('Bu iş için henüz dosya yüklenmemiş.');
|
||
return ActionFiles(files);
|
||
}
|
||
|
||
static Future<ActionOutcome> _addMember(
|
||
Map<String, String> p,
|
||
TenantMembership membership,
|
||
) async {
|
||
final email = p['email'];
|
||
final firstName = p['first_name'];
|
||
final lastName = p['last_name'] ?? '';
|
||
final role = p['role'];
|
||
final password = p['password'];
|
||
|
||
if (email == null || firstName == null || role == null || password == null) {
|
||
return ActionError('Eksik bilgi: e-posta, ad, rol ve şifre gerekli.');
|
||
}
|
||
|
||
await TenantTeamRepository.instance.addMember(
|
||
tenantId: membership.tenant.id,
|
||
email: email,
|
||
password: password,
|
||
firstName: firstName,
|
||
lastName: lastName,
|
||
role: TenantMembership.parseRole(role),
|
||
);
|
||
return ActionSuccess('$firstName $lastName ekibe eklendi.');
|
||
}
|
||
}
|