This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:trainhub_flutter/core/constants/ai_constants.dart';
|
||||
import 'package:trainhub_flutter/domain/entities/training_exercise.dart';
|
||||
import 'package:trainhub_flutter/domain/entities/training_section.dart';
|
||||
import 'package:trainhub_flutter/domain/repositories/chat_repository.dart';
|
||||
import 'package:trainhub_flutter/domain/repositories/exercise_repository.dart';
|
||||
import 'package:trainhub_flutter/domain/repositories/note_repository.dart';
|
||||
@@ -206,6 +210,71 @@ class ChatController extends _$ChatController {
|
||||
/// kept and persisted like a normal reply.
|
||||
void stopGeneration() => _cancelToken?.cancel();
|
||||
|
||||
/// Turns an assistant reply describing a workout into a real training plan
|
||||
/// in the database. The model re-reads its own text and emits strict JSON,
|
||||
/// which is parsed into sections/exercises. Returns the created plan name.
|
||||
Future<String> createPlanFromText(String content) async {
|
||||
const extractPrompt =
|
||||
'You convert workout descriptions into strict JSON. '
|
||||
'Respond with ONLY valid JSON — no markdown fences, no commentary. '
|
||||
'Schema: {"name": string, "sections": [{"name": string, '
|
||||
'"exercises": [{"name": string, "sets": int, "value": int, '
|
||||
'"isTime": bool, "rest": int}]}]}. '
|
||||
'"value" means repetitions when isTime=false, or seconds when '
|
||||
'isTime=true. "rest" is rest between sets in seconds. '
|
||||
'Keep exercise names short. If the text contains no workout plan, '
|
||||
'respond with {"name": "", "sections": []}.';
|
||||
|
||||
final buffer = StringBuffer();
|
||||
await for (final delta in _llm.streamChat([
|
||||
{'role': 'system', 'content': extractPrompt},
|
||||
{'role': 'user', 'content': content},
|
||||
])) {
|
||||
buffer.write(delta);
|
||||
}
|
||||
|
||||
final raw = buffer.toString();
|
||||
final start = raw.indexOf('{');
|
||||
final end = raw.lastIndexOf('}');
|
||||
if (start < 0 || end <= start) {
|
||||
throw Exception('The model did not return a valid plan.');
|
||||
}
|
||||
final data =
|
||||
jsonDecode(raw.substring(start, end + 1)) as Map<String, dynamic>;
|
||||
final sectionsJson = (data['sections'] as List?) ?? [];
|
||||
if (sectionsJson.isEmpty) {
|
||||
throw Exception('No workout plan found in this reply.');
|
||||
}
|
||||
|
||||
final sections = <TrainingSectionEntity>[
|
||||
for (final s in sectionsJson)
|
||||
TrainingSectionEntity(
|
||||
id: const Uuid().v4(),
|
||||
name: (s['name'] ?? 'Section') as String,
|
||||
exercises: [
|
||||
for (final e in (s['exercises'] as List? ?? []))
|
||||
TrainingExerciseEntity(
|
||||
instanceId: const Uuid().v4(),
|
||||
exerciseId: '',
|
||||
name: (e['name'] ?? 'Exercise') as String,
|
||||
sets: (e['sets'] as num?)?.toInt() ?? 3,
|
||||
value: (e['value'] as num?)?.toInt() ?? 10,
|
||||
isTime: (e['isTime'] as bool?) ?? false,
|
||||
rest: (e['rest'] as num?)?.toInt() ?? 60,
|
||||
),
|
||||
],
|
||||
),
|
||||
];
|
||||
|
||||
final planRepo = getIt<TrainingPlanRepository>();
|
||||
final name = ((data['name'] as String?)?.trim().isEmpty ?? true)
|
||||
? 'AI Plan'
|
||||
: (data['name'] as String).trim();
|
||||
final created = await planRepo.create(name);
|
||||
await planRepo.update(created.copyWith(sections: sections));
|
||||
return name;
|
||||
}
|
||||
|
||||
Future<String> _streamResponse(
|
||||
String systemPrompt,
|
||||
List<Map<String, String>> history,
|
||||
|
||||
Reference in New Issue
Block a user