import 'dart:convert';
import 'package:http/http.dart' as http;
import 'ai_settings_store.dart';
class AiMaterialSuggestion {
const AiMaterialSuggestion({
required this.name,
required this.quantity,
required this.unit,
});
final String name;
final String quantity;
final String unit;
factory AiMaterialSuggestion.fromJson(Map<String, dynamic> json) =>
AiMaterialSuggestion(
name: json['name'] as String? ?? '',
quantity: json['quantity']?.toString() ?? '',
unit: json['unit'] as String? ?? 'Stk.',
);
}
class AiReportSuggestion {
const AiReportSuggestion({
required this.activities,
required this.materials,
required this.additionalOrder,
required this.openQuestions,
required this.confidence,
this.startTime,
this.endTime,
this.breakMinutes,
});
final List<String> activities;
final List<AiMaterialSuggestion> materials;
final String additionalOrder;
final List<String> openQuestions;
final double confidence;
final String? startTime;
final String? endTime;
final int? breakMinutes;
factory AiReportSuggestion.fromJson(Map<String, dynamic> json) =>
AiReportSuggestion(
activities: (json['activities'] as List<dynamic>? ?? const [])
.whereType<String>()
.where((value) => value.trim().isNotEmpty)
.toList(),
materials: (json['materials'] as List<dynamic>? ?? const [])
.whereType<Map<String, dynamic>>()
.map(AiMaterialSuggestion.fromJson)
.where((value) => value.name.trim().isNotEmpty)
.toList(),
additionalOrder: json['additionalOrder'] as String? ?? '',
openQuestions: (json['openQuestions'] as List<dynamic>? ?? const [])
.whereType<String>()
.toList(),
confidence: (json['confidence'] as num?)?.toDouble() ?? 0,
startTime: json['startTime'] as String?,
endTime: json['endTime'] as String?,
breakMinutes: json['breakMinutes'] as int?,
);
}
class AiReportRequest {
const AiReportRequest({
required this.notes,
required this.customer,
required this.site,
});
final String notes;
final String customer;
final String site;
}
class AiAssistantException implements Exception {
const AiAssistantException(this.message);
final String message;
@override
String toString() => message;
}
abstract interface class AiReportAssistant {
Future<AiReportSuggestion> suggest(AiReportRequest request);
}
class RemoteAiReportAssistant implements AiReportAssistant {
RemoteAiReportAssistant({http.Client? client})
: _client = client ?? http.Client();
final http.Client _client;
static const outputSchema = <String, Object?>{
'type': 'object',
'additionalProperties': false,
'properties': {
'activities': {
'type': 'array',
'items': {'type': 'string'},
},
'materials': {
'type': 'array',
'items': {
'type': 'object',
'additionalProperties': false,
'properties': {
'name': {'type': 'string'},
'quantity': {'type': 'string'},
'unit': {'type': 'string'},
},
'required': ['name', 'quantity', 'unit'],
},
},
'additionalOrder': {'type': 'string'},
'openQuestions': {
'type': 'array',
'items': {'type': 'string'},
},
'confidence': {'type': 'number', 'minimum': 0, 'maximum': 1},
'startTime': {
'type': ['string', 'null'],
},
'endTime': {
'type': ['string', 'null'],
},
'breakMinutes': {
'type': ['integer', 'null'],
},
},
'required': [
'activities',
'materials',
'additionalOrder',
'openQuestions',
'confidence',
'startTime',
'endTime',
'breakMinutes',
],
};
@override
Future<AiReportSuggestion> suggest(AiReportRequest request) async {
final settings = AiSettingsStore.instance.settings;
if (!settings.enabled) {
throw const AiAssistantException('KI-Unterstützung ist deaktiviert.');
}
if (!settings.usesProxy) {
throw const AiAssistantException(
'Für KI-Anfragen ist eine sichere Server-/Proxy-URL erforderlich.',
);
}
final response = await _proxyRequest(settings, request);
try {
return AiReportSuggestion.fromJson(
jsonDecode(response) as Map<String, dynamic>,
);
} on Object {
throw const AiAssistantException(
'Die KI-Antwort hatte ein unerwartetes Format.',
);
}
}
Future<String> _proxyRequest(
AiSettings settings,
AiReportRequest request,
) async {
final base = settings.proxyUrl.trim().replaceFirst(RegExp(r'/+$'), '');
final response = await _client
.post(
Uri.parse('$base/[INTERNAL_AI_ENDPOINT]'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({
'provider': settings.provider.name,
'model': settings.activeModel,
'input': _input(request),
'schema': outputSchema,
}),
)
.timeout(const Duration(seconds: 35));
final json = _checkedJson(response);
final suggestion = json['suggestion'];
return suggestion is String ? suggestion : jsonEncode(suggestion ?? json);
}
Map<String, dynamic> _checkedJson(http.Response response) {
Map<String, dynamic> json;
try {
json =
jsonDecode(utf8.decode(response.bodyBytes)) as Map<String, dynamic>;
} on Object {
throw AiAssistantException(
'Der KI-Dienst antwortete ungültig (${response.statusCode}).',
);
}
if (response.statusCode < 200 || response.statusCode >= 300) {
final error = json['error'];
final message = error is Map<String, dynamic>
? error['message']?.toString()
: null;
throw AiAssistantException(
message ?? 'KI-Anfrage fehlgeschlagen (${response.statusCode}).',
);
}
return json;
}
String _input(AiReportRequest request) =>
'Kunde: ${request.customer}\nBaustelle: ${request.site}\nRohe Notizen des Monteurs:\n${request.notes}';
}
