import 'dart:async'; import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import 'package:shared_preferences/shared_preferences.dart'; import '../core/network/api_config.dart'; import 'story_detail_page.dart'; class StoryLoadingPage extends StatefulWidget { /// Selected story elements from the generator modal final List characters; final List scenes; final List props; const StoryLoadingPage({ super.key, this.characters = const [], this.scenes = const [], this.props = const [], }); @override State createState() => _StoryLoadingPageState(); } class _StoryLoadingPageState extends State { double _progress = 0.0; String _loadingText = '正在收集灵感碎片...'; bool _hasError = false; @override void initState() { super.initState(); _generateStory(); } Future _getToken() async { final prefs = await SharedPreferences.getInstance(); return prefs.getString('access_token'); } Future _generateStory() async { try { final token = await _getToken(); final request = http.Request( 'POST', Uri.parse('${ApiConfig.fullBaseUrl}/stories/generate/'), ); request.headers['Content-Type'] = 'application/json'; if (token != null) { request.headers['Authorization'] = 'Bearer $token'; } request.body = jsonEncode({ 'characters': widget.characters, 'scenes': widget.scenes, 'props': widget.props, }); final client = http.Client(); final response = await client.send(request).timeout( const Duration(seconds: 180), ); if (response.statusCode != 200) { _showError('服务器响应异常 (${response.statusCode})'); client.close(); return; } // ── Parse SSE stream ── String buffer = ''; String? storyTitle; String? storyContent; await for (final chunk in response.stream.transform(utf8.decoder)) { buffer += chunk; while (buffer.contains('\n\n')) { final idx = buffer.indexOf('\n\n'); final block = buffer.substring(0, idx).trim(); buffer = buffer.substring(idx + 2); // Extract the data line from the SSE block (may contain event: + data:) String? jsonStr; for (final line in block.split('\n')) { if (line.startsWith('data: ')) { jsonStr = line.substring(6); break; } } if (jsonStr == null) continue; try { final event = jsonDecode(jsonStr) as Map; final stage = event['stage'] as String? ?? ''; final progress = (event['progress'] as num?)?.toDouble() ?? 0; final message = event['message'] as String? ?? ''; if (!mounted) return; switch (stage) { case 'connecting': _updateProgress(progress / 100, '正在收集灵感碎片...'); break; case 'generating': _updateProgress(progress / 100, '故事正在诞生...'); break; case 'parsing': _updateProgress(progress / 100, '正在编制最后的魔法...'); break; case 'done': storyTitle = event['title'] as String? ?? '卡皮巴拉的故事'; storyContent = event['content'] as String? ?? ''; _updateProgress(1.0, '大功告成!'); break; case 'error': _showError(message.isNotEmpty ? message : '故事生成失败,请重试'); client.close(); return; } } catch (e) { debugPrint('SSE parse error: $e'); } } } client.close(); // ── Navigate to story detail ── if (!mounted) return; if (storyTitle != null && storyContent != null && storyContent.isNotEmpty) { // Brief pause to show "大功告成!" await Future.delayed(const Duration(milliseconds: 600)); if (!mounted) return; final result = await Navigator.of(context).push( MaterialPageRoute( builder: (context) => StoryDetailPage( mode: StoryMode.generated, story: { 'title': storyTitle, 'content': storyContent, }, ), ), ); // Pass the story data back to DeviceControlPage if (mounted) { if (result == 'saved') { Navigator.of(context).pop({ 'action': 'saved', 'title': storyTitle, 'content': storyContent, }); } else { Navigator.of(context).pop(result); } } } else { _showError('AI 返回了空故事,请重试'); } } catch (e) { debugPrint('Story generation error: $e'); if (mounted) { _showError('网络开小差了,再试一次~'); } } } void _updateProgress(double progress, String text) { if (!mounted) return; setState(() { _progress = progress.clamp(0.0, 1.0); _loadingText = text; }); } void _showError(String message) { if (!mounted) return; setState(() { _hasError = true; _loadingText = message; }); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: const Color(0xFFFDF9F3), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ // Image Image.asset( 'assets/www/kapi_writing.png', width: 200, height: 200, errorBuilder: (c, e, s) => const Icon( Icons.edit_note, size: 100, color: Color(0xFFD1D5DB), ), ), const SizedBox(height: 32), // Text Text( _loadingText, style: const TextStyle( fontSize: 18, color: Color(0xFF4B2404), fontWeight: FontWeight.w600, ), textAlign: TextAlign.center, ), const SizedBox(height: 24), // Progress Bar Container( width: 280, height: 12, decoration: BoxDecoration( color: const Color(0xFFC99672).withOpacity(0.2), borderRadius: BorderRadius.circular(6), ), child: ClipRRect( borderRadius: BorderRadius.circular(6), child: FractionallySizedBox( alignment: Alignment.centerLeft, widthFactor: _progress.clamp(0.0, 1.0), child: Container( decoration: const BoxDecoration( gradient: LinearGradient( colors: [Color(0xFFECCFA8), Color(0xFFC99672)], ), ), ), ), ), ), // Retry button (shown on error) if (_hasError) ...[ const SizedBox(height: 32), TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text( '返回重试', style: TextStyle( fontSize: 16, color: Color(0xFFC99672), fontWeight: FontWeight.w600, ), ), ), ], ], ), ), ); } }