You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

265 line
9.1KB

  1. // lib/presentation/screens/text_generation/text_generation_screen.dart
  2. import 'dart:convert';
  3. import 'package:flutter/material.dart';
  4. import '../../../repositories/ai_repository.dart';
  5. import '../../../routes/app_routes.dart';
  6. import '../../widgets/creation_flow_layout.dart';
  7. import '../post_refinement/post_refinement_screen.dart';
  8. class TextGenerationScreenArguments {
  9. TextGenerationScreenArguments({
  10. required this.imageBase64,
  11. required this.aiRepository,
  12. });
  13. final String imageBase64;
  14. final AiRepository aiRepository;
  15. }
  16. final class TextGenerationScreen extends StatefulWidget {
  17. const TextGenerationScreen({required this.arguments, super.key});
  18. final TextGenerationScreenArguments arguments;
  19. @override
  20. State<TextGenerationScreen> createState() => _TextGenerationScreenState();
  21. }
  22. class _TextGenerationScreenState extends State<TextGenerationScreen> {
  23. bool _loading = false;
  24. List<String> _generatedIdeas = [];
  25. final List<String> _logs = [];
  26. final _professionController = TextEditingController(text: 'Entrepreneur digital');
  27. final _toneController = TextEditingController(text: 'Professionnel et engageant');
  28. @override
  29. void initState() {
  30. super.initState();
  31. _addLog('🖼️ Image reçue avec succès.');
  32. }
  33. void _addLog(String log) {
  34. if (mounted) {
  35. setState(() => _logs.insert(0, log));
  36. }
  37. }
  38. @override
  39. void dispose() {
  40. _professionController.dispose();
  41. _toneController.dispose();
  42. super.dispose();
  43. }
  44. // --- MÉTHODE CORRIGÉE ET SIMPLIFIÉE ---
  45. Future<void> _handleGenerateIdeas() async {
  46. if (_loading) {
  47. _addLog('⏳ Annulation : Génération déjà en cours.');
  48. return;
  49. }
  50. if (!mounted) return;
  51. setState(() {
  52. _loading = true;
  53. _generatedIdeas = [];
  54. _logs.clear();
  55. });
  56. _addLog("▶️ Lancement de la génération d'idées...");
  57. try {
  58. final profession = _professionController.text;
  59. final tone = _toneController.text;
  60. _addLog("⚙️ Paramètres : Métier='$profession', Ton='$tone'.");
  61. _addLog('🚀 Appel du AiRepository vers Ollama...');
  62. // 1. On récupère directement la liste de chaînes de caractères.
  63. final ideas = await widget.arguments.aiRepository.generatePostIdeas(
  64. base64Image: widget.arguments.imageBase64,
  65. profession: profession,
  66. tone: tone,
  67. );
  68. if (!mounted) return;
  69. _addLog("✅ Succès ! Réponse reçue d'Ollama.");
  70. // 2. La logique de découpage ('split') est supprimée car inutile.
  71. if (ideas.isEmpty) {
  72. _addLog('⚠️ Avertissement : Le service a retourné une liste vide.');
  73. } else {
  74. _addLog('📦 ${ideas.length} idée(s) reçue(s).');
  75. }
  76. // 3. On met à jour l'état directement avec la liste reçue.
  77. setState(() {
  78. _generatedIdeas = ideas;
  79. });
  80. _addLog('✨ Interface mise à jour avec la liste des idées.');
  81. } catch (e) {
  82. if (!mounted) return;
  83. _addLog('❌ ERREUR : ${e.toString()}');
  84. ScaffoldMessenger.of(context).showSnackBar(SnackBar(
  85. content: Text('Erreur: ${e.toString()}'),
  86. backgroundColor: Colors.red));
  87. } finally {
  88. if (mounted) {
  89. setState(() => _loading = false);
  90. }
  91. _addLog('⏹️ Fin du processus.');
  92. }
  93. }
  94. void _navigateToRefinementScreen(String selectedIdea) {
  95. Navigator.pushNamed(
  96. context,
  97. AppRoutes.postRefinement,
  98. arguments: PostRefinementScreenArguments(
  99. initialText: selectedIdea,
  100. imageBase64: widget.arguments.imageBase64,
  101. aiRepository: widget.arguments.aiRepository,
  102. ),
  103. );
  104. }
  105. @override
  106. Widget build(BuildContext context) {
  107. // Le reste du fichier build est identique et correct.
  108. return CreationFlowLayout(
  109. currentStep: 4,
  110. title: '4. Génération de Texte',
  111. child: Stack(
  112. children: [
  113. SingleChildScrollView(
  114. padding: const EdgeInsets.all(16),
  115. child: Column(
  116. crossAxisAlignment: CrossAxisAlignment.start,
  117. children: [
  118. Center(
  119. child: ConstrainedBox(
  120. constraints: const BoxConstraints(maxHeight: 200),
  121. child: ClipRRect(
  122. borderRadius: BorderRadius.circular(12),
  123. child: Image.memory(
  124. base64Decode(widget.arguments.imageBase64),
  125. width: double.infinity,
  126. fit: BoxFit.cover,
  127. ),
  128. ),
  129. ),
  130. ),
  131. const SizedBox(height: 24),
  132. TextField(
  133. controller: _professionController,
  134. decoration: const InputDecoration(
  135. labelText: 'Votre métier',
  136. border: OutlineInputBorder(),
  137. prefixIcon: Icon(Icons.work_outline))),
  138. const SizedBox(height: 16),
  139. TextField(
  140. controller: _toneController,
  141. decoration: const InputDecoration(
  142. labelText: 'Ton souhaité',
  143. border: OutlineInputBorder(),
  144. prefixIcon: Icon(Icons.campaign_outlined))),
  145. const SizedBox(height: 24),
  146. SizedBox(
  147. width: double.infinity,
  148. child: FilledButton.icon(
  149. onPressed: _loading ? null : _handleGenerateIdeas,
  150. icon: _loading
  151. ? const SizedBox(
  152. width: 20,
  153. height: 20,
  154. child: CircularProgressIndicator(
  155. color: Colors.white, strokeWidth: 2))
  156. : const Icon(Icons.auto_awesome),
  157. label: const Text('Générer 3 idées de post'),
  158. style: FilledButton.styleFrom(
  159. padding: const EdgeInsets.symmetric(vertical: 16)),
  160. ),
  161. ),
  162. const SizedBox(height: 24),
  163. if (_generatedIdeas.isNotEmpty) ...[
  164. const Divider(height: 32),
  165. Text('Choisissez une idée à affiner',
  166. style: Theme.of(context)
  167. .textTheme
  168. .titleMedium
  169. ?.copyWith(fontWeight: FontWeight.bold)),
  170. const SizedBox(height: 16),
  171. ListView.builder(
  172. shrinkWrap: true,
  173. physics: const NeverScrollableScrollPhysics(),
  174. itemCount: _generatedIdeas.length,
  175. itemBuilder: (context, index) {
  176. final idea = _generatedIdeas[index];
  177. return Card(
  178. margin: const EdgeInsets.only(bottom: 12),
  179. child: ListTile(
  180. title: Text(idea,
  181. maxLines: 5, // On peut garder plus de lignes
  182. overflow: TextOverflow.ellipsis),
  183. leading:
  184. CircleAvatar(child: Text('${index + 1}')),
  185. trailing: const Icon(Icons.edit_note),
  186. onTap: () => _navigateToRefinementScreen(idea)));
  187. },
  188. ),
  189. ],
  190. const SizedBox(height: 150),
  191. ],
  192. ),
  193. ),
  194. Positioned(
  195. bottom: 0,
  196. left: 0,
  197. right: 0,
  198. child: IgnorePointer(
  199. child: Container(
  200. height: 140,
  201. padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
  202. decoration: BoxDecoration(
  203. gradient: LinearGradient(
  204. colors: [
  205. Theme.of(context).scaffoldBackgroundColor.withOpacity(0),
  206. Theme.of(context).scaffoldBackgroundColor.withOpacity(0.6),
  207. Theme.of(context).scaffoldBackgroundColor,
  208. ],
  209. begin: Alignment.topCenter,
  210. end: Alignment.bottomCenter,
  211. ),
  212. ),
  213. child: ListView.builder(
  214. reverse: true,
  215. itemCount: _logs.length,
  216. itemBuilder: (context, index) {
  217. final log = _logs[index];
  218. return Text(log, style: TextStyle(
  219. color: log.contains('❌')
  220. ? Theme.of(context).colorScheme.error
  221. : (log.contains('✅') || log.contains('📦') || log.contains('✨'))
  222. ? Colors.green[600]
  223. : Theme.of(context)
  224. .colorScheme
  225. .onSurface
  226. .withOpacity(0.8),
  227. fontSize: 12
  228. ),
  229. );
  230. },
  231. ),
  232. ),
  233. ),
  234. ),
  235. ],
  236. ),
  237. );
  238. }
  239. }