diff --git a/.gitignore b/.gitignore index 765ff92..bf43334 100644 Binary files a/.gitignore and b/.gitignore differ diff --git a/ISSUES.md b/ISSUES.md index 8c889b7..fa2f9ad 100644 --- a/ISSUES.md +++ b/ISSUES.md @@ -9,8 +9,8 @@ ## MEDIUM PRIORITY (Code Quality & Cleanup) - [ ] **Corrupted/Duplicate Configuration**: `stemly_app/pubspec.yaml` contains a large block of commented-out/duplicate YAML at the beginning. - *Action*: Clean up the file to only include the active configuration. -- [ ] **Backup Files**: `stemly_app/lib/screens/scan_result_screen.dart.backup` should be removed from the repository. -- [ ] **Unused Files**: `backend/test_output.txt` and `backend/test_image.png` appear to be artifacts from testing that should probably be ignored or removed. +- [x] **Backup Files**: `stemly_app/lib/screens/scan_result_screen.dart.backup` should be removed from the repository. +- [x] **Unused Files**: `backend/test_output.txt` and `backend/test_image.png` appear to be artifacts from testing that should probably be ignored or removed. ## LOW PRIORITY (Technical Debt) - [ ] **TODOs**: diff --git a/backend/test_image.png b/backend/test_image.png deleted file mode 100644 index 06d7405..0000000 Binary files a/backend/test_image.png and /dev/null differ diff --git a/backend/test_output.txt b/backend/test_output.txt deleted file mode 100644 index 8a10017..0000000 Binary files a/backend/test_output.txt and /dev/null differ diff --git a/stemly_app/lib/screens/scan_result_screen.dart.backup b/stemly_app/lib/screens/scan_result_screen.dart.backup deleted file mode 100644 index 2f1c58f..0000000 --- a/stemly_app/lib/screens/scan_result_screen.dart.backup +++ /dev/null @@ -1,1405 +0,0 @@ -import 'dart:io'; -import 'dart:convert'; -import 'package:flutter/material.dart'; -import 'package:http/http.dart' as http; -import 'package:stemly_app/visualiser/kinematics_component.dart'; -import 'package:stemly_app/visualiser/optics_component.dart'; - -import '../visualiser/projectile_motion.dart'; -import '../visualiser/free_fall_component.dart'; -import '../visualiser/shm_component.dart'; -import '../visualiser/visualiser_models.dart'; - -class ScanResultScreen extends StatefulWidget { - final String topic; - final List variables; - final Map notesJson; - final String imagePath; - - const ScanResultScreen({ - super.key, - required this.topic, - required this.variables, - required this.notesJson, - required this.imagePath, - }); - - @override - State createState() => _ScanResultScreenState(); -} - -class _ScanResultScreenState extends State { - final Map expanded = {}; - - VisualTemplate? visualiserTemplate; - Widget? visualiserWidget; - bool loadingVisualiser = true; - - final String serverIp = "http://10.0.2.2:8000"; - - @override - void initState() { - super.initState(); - - // Debug logging - print("🔍 ScanResultScreen initialized"); - print("🔍 Topic: ${widget.topic}"); - print("🔍 Variables: ${widget.variables}"); - print("🔍 Image Path: ${widget.imagePath}"); - print("🔍 Notes JSON type: ${widget.notesJson.runtimeType}"); - print("🔍 Notes JSON keys: ${widget.notesJson.keys.toList()}"); - print("🔍 Notes JSON isEmpty: ${widget.notesJson.isEmpty}"); - - for (var key in widget.notesJson.keys) { - expanded[key] = false; - print("🔍 Notes key '$key' has value type: ${widget.notesJson[key].runtimeType}"); - } - - _loadVisualiser(); - } - - Future _loadVisualiser() async { - setState(() => loadingVisualiser = true); - - try { - final url = Uri.parse('$serverIp/visualiser/generate'); - final response = await http.post( - url, - headers: {'Content-Type': 'application/json'}, - body: jsonEncode({ - 'topic': widget.topic, - 'variables': widget.variables, - }), - ); - - if (response.statusCode == 200) { - final data = jsonDecode(response.body); - final templateJson = data['template'] as Map; - final template = VisualTemplate.fromJson(templateJson); - - final templateId = template.templateId.toLowerCase(); - Widget? newWidget; - - final p = template.parameters; - double getVal(String key) => p[key]?.value ?? 0.0; - - if (templateId.contains('projectile')) { - newWidget = ProjectileMotionWidget( - U: getVal('U'), - theta: getVal('theta'), - g: getVal('g'), - ); - } else if (templateId.contains('free') || templateId.contains('fall')) { - newWidget = FreeFallWidget( - h: getVal('h'), - g: getVal('g'), - ); - } else if (templateId.contains('shm')) { - newWidget = SHMWidget( - A: getVal('A'), - m: getVal('m'), - k: getVal('k'), - ); - } else if (templateId.contains('kinematics')) { - newWidget = KinematicsWidget( - u: getVal('u'), - a: getVal('a'), - tMax: getVal('t_max'), - ); - } else if (templateId.contains('optics')) { - newWidget = OpticsWidget( - f: getVal('f'), - u: getVal('u'), - h_o: getVal('h_o'), - ); - } - - if (mounted) { - setState(() { - visualiserTemplate = template; - visualiserWidget = newWidget; - loadingVisualiser = false; - }); - } - } else { - setState(() => loadingVisualiser = false); - } - } catch (e) { - setState(() => loadingVisualiser = false); - } - } - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final cs = theme.colorScheme; - - final deepBlue = cs.primary; - final primaryColor = cs.primaryContainer; - final cardColor = theme.cardColor; - final background = theme.scaffoldBackgroundColor; - - return DefaultTabController( - length: 2, - child: Scaffold( - backgroundColor: background, - - // --------------------------------------------------------- - // NEW POLISHED APP BAR - // --------------------------------------------------------- - appBar: AppBar( - elevation: 0, - backgroundColor: primaryColor, - iconTheme: IconThemeData(color: deepBlue), - - title: Text( - "Scan Result", - style: TextStyle( - color: deepBlue, - fontWeight: FontWeight.w700, - fontSize: 22, - letterSpacing: 0.3, - ), - ), - centerTitle: true, - - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - bottom: Radius.circular(26), - ), - ), - - bottom: PreferredSize( - preferredSize: const Size.fromHeight(65), - child: Padding( - padding: const EdgeInsets.only(bottom: 10, left: 16, right: 16), - child: Container( - height: 48, - decoration: BoxDecoration( - color: deepBlue.withOpacity(0.10), - borderRadius: BorderRadius.circular(30), - ), - child: TabBar( - dividerColor: Colors.transparent, - labelColor: Colors.white, - unselectedLabelColor: deepBlue.withOpacity(0.7), - - indicator: BoxDecoration( - borderRadius: BorderRadius.circular(30), - gradient: LinearGradient( - colors: [ - deepBlue, - deepBlue.withOpacity(0.85), - ], - ), - boxShadow: [ - BoxShadow( - color: deepBlue.withOpacity(0.3), - blurRadius: 8, - offset: const Offset(0, 3), - ) - ], - ), - - indicatorSize: TabBarIndicatorSize.tab, - labelStyle: const TextStyle( - fontWeight: FontWeight.w700, - fontSize: 15, - ), - unselectedLabelStyle: const TextStyle( - fontWeight: FontWeight.w600, - fontSize: 14, - ), - - tabs: const [ - Tab(text: "AI Visualiser"), - Tab(text: "AI Notes"), - ], - ), - ), - ), - ), - ), - - // --------------------------------------------------------- - // BODY - // --------------------------------------------------------- - body: TabBarView( - children: [ - _visualiser(deepBlue), - _notes(cardColor, deepBlue), - - setState(() { - _chatMessages.add({'role': 'user', 'content': text}); - _isSendingMessage = true; - _chatController.clear(); - }); - - try { - final currentParams = {}; - if (visualiserTemplate != null) { - visualiserTemplate!.parameters.forEach((k, v) => currentParams[k] = v.value); - } - - final res = await http.post( - Uri.parse("$serverIp/visualiser/update"), - headers: {"Content-Type": "application/json"}, - body: jsonEncode({ - "template_id": visualiserTemplate?.templateId ?? "", - "parameters": currentParams, - "user_prompt": text, - }), - ); - - if (res.statusCode == 200) { - final data = jsonDecode(res.body); - final params = data['parameters'] as Map; - final aiResponse = data['ai_response']; - - _updateVisualiserWidget(visualiserTemplate!.templateId, params); - - setState(() { - _chatMessages.add({'role': 'ai', 'content': aiResponse}); - _isSendingMessage = false; - }); - } - } catch (e) { - setState(() { - _chatMessages.add({'role': 'ai', 'content': "Connection failed"}); - _isSendingMessage = false; - }); - } - } - - void _updateVisualiserWidget(String templateId, Map params) { - double getVal(String k) => - (params[k] is num) ? (params[k] as num).toDouble() : 0.0; - - Widget? newWidget; - final id = templateId.toLowerCase(); - - if (id.contains("projectile")) { - newWidget = ProjectileMotionWidget( - U: getVal("U"), - theta: getVal("theta"), - g: getVal("g"), - ); - } else if (id.contains("free")) { - newWidget = FreeFallWidget( - h: getVal("h"), - g: getVal("g"), - ); - } else if (id.contains("shm")) { - newWidget = SHMWidget( - A: getVal("A"), - m: getVal("m"), - k: getVal("k"), - ); - } - - setState(() => visualiserWidget = newWidget); - } - - // --------------------------------------------------------- - // NOTES - // --------------------------------------------------------- - Widget _notes(Color cardColor, Color deepBlue) { - // Debug logging - print("📝 Building notes UI"); - print("📝 Notes JSON keys: ${widget.notesJson.keys.toList()}"); - print("📝 Notes JSON: ${widget.notesJson}"); - - // Check for error in response - if (widget.notesJson.containsKey("error")) { - return Center( - child: Padding( - padding: const EdgeInsets.all(24), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.error_outline, size: 64, color: deepBlue.withOpacity(0.5)), - const SizedBox(height: 16), - Text( - "Failed to load notes", - style: TextStyle( - color: deepBlue, - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 8), - Text( - widget.notesJson["error"].toString(), - style: TextStyle(color: deepBlue.withOpacity(0.7), fontSize: 14), - textAlign: TextAlign.center, - ), - ], - ), - ), - ); - } - - // Check for empty notes - if (widget.notesJson.isEmpty) { - return Center( - child: Padding( - padding: const EdgeInsets.all(24), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.note_outlined, size: 64, color: deepBlue.withOpacity(0.5)), - const SizedBox(height: 16), - Text( - "No notes available", - style: TextStyle( - color: deepBlue, - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 8), - Text( - "Notes generation may have failed", - style: TextStyle(color: deepBlue.withOpacity(0.7), fontSize: 14), - textAlign: TextAlign.center, - ), - ], - ), - ), - ); - } - - return SingleChildScrollView( - padding: const EdgeInsets.all(16), - child: Column( - children: widget.notesJson.entries.map((entry) { - final key = entry.key; - - return _expandableCard( - title: _formatKey(key), - expanded: expanded[key]!, - onTap: () => setState(() => expanded[key] = !expanded[key]!), - child: _buildContent(entry.value, deepBlue), - cardColor: cardColor, - deepBlue: deepBlue, - ); - }).toList(), - ), - ); - } - - Widget _expandableCard({ - required String title, - required bool expanded, - required VoidCallback onTap, - required Widget child, - required Color cardColor, - required Color deepBlue, - }) { - return AnimatedContainer( - duration: const Duration(milliseconds: 260), - margin: const EdgeInsets.only(bottom: 16), - decoration: BoxDecoration( - color: cardColor, - borderRadius: BorderRadius.circular(18), - ), - child: Column( - children: [ - InkWell( - borderRadius: BorderRadius.circular(18), - onTap: onTap, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - title, - style: TextStyle( - fontSize: 18, - color: deepBlue, - fontWeight: FontWeight.w700, - ), - ), - Icon( - expanded - ? Icons.keyboard_arrow_up_rounded - : Icons.keyboard_arrow_down_rounded, - size: 30, - color: deepBlue, - ), - ], - ), - ), - ), - - AnimatedCrossFade( - duration: const Duration(milliseconds: 260), - crossFadeState: expanded - ? CrossFadeState.showSecond - : CrossFadeState.showFirst, - firstChild: const SizedBox.shrink(), - secondChild: - Padding(padding: const EdgeInsets.all(14), child: child), - ), - ], - ), - ); - } - - Widget _buildContent(dynamic value, Color deepBlue) { - if (value is String) { - return Text(value, style: TextStyle(fontSize: 15, color: deepBlue)); - } - - if (value is List) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: value - .map((v) => Padding( - padding: const EdgeInsets.only(bottom: 8), - child: Text("• $v", - style: TextStyle(fontSize: 15, color: deepBlue)), - )) - .toList(), - ); - } - - if (value is Map) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: value.entries - .map((e) => Padding( - padding: const EdgeInsets.only(bottom: 8), - child: Text("${e.key}: ${e.value}", - style: TextStyle(fontSize: 15, color: deepBlue)), - )) - .toList(), - ); - } - - return const Text("Unsupported format"); - } - - Widget _title(String text, Color deepBlue) => Text( - text, - style: TextStyle( - fontSize: 22, - fontWeight: FontWeight.bold, - color: deepBlue, - ), - ); - - Widget _value(String text, Color deepBlue) => - Text(text, style: TextStyle(fontSize: 17, color: deepBlue)); - - String _formatKey(String raw) { - if (raw.isEmpty) return ""; - return raw - .replaceAll("_", " ") - .trim() - .replaceFirst(raw[0], raw[0].toUpperCase()); - } -} - - - -//------------------------------------------------- - -import 'dart:io'; -import 'dart:convert'; -import 'package:flutter/material.dart'; -import 'package:http/http.dart' as http; -import 'package:stemly_app/visualiser/kinematics_component.dart'; -import 'package:stemly_app/visualiser/optics_component.dart'; - -import '../visualiser/projectile_motion.dart'; -import '../visualiser/free_fall_component.dart'; -import '../visualiser/shm_component.dart'; -import '../visualiser/visualiser_models.dart'; - -class ScanResultScreen extends StatefulWidget { - final String topic; - final List variables; - final Map notesJson; - final String imagePath; - - const ScanResultScreen({ - super.key, - required this.topic, - required this.variables, - required this.notesJson, - required this.imagePath, - }); - - @override - State createState() => _ScanResultScreenState(); -} - -class _ScanResultScreenState extends State { - final Map expanded = {}; - - VisualTemplate? visualiserTemplate; - Widget? visualiserWidget; - bool loadingVisualiser = true; - - final String serverIp = "http://10.0.2.2:8000"; - - @override - void initState() { - super.initState(); - - // Debug logging - print("🔍 ScanResultScreen initialized"); - print("🔍 Topic: ${widget.topic}"); - print("🔍 Variables: ${widget.variables}"); - print("🔍 Image Path: ${widget.imagePath}"); - print("🔍 Notes JSON type: ${widget.notesJson.runtimeType}"); - print("🔍 Notes JSON keys: ${widget.notesJson.keys.toList()}"); - print("🔍 Notes JSON isEmpty: ${widget.notesJson.isEmpty}"); - - for (var key in widget.notesJson.keys) { - expanded[key] = false; - print("🔍 Notes key '$key' has value type: ${widget.notesJson[key].runtimeType}"); - } - - _loadVisualiser(); - } - - Future _loadVisualiser() async { - setState(() => loadingVisualiser = true); - - try { - final url = Uri.parse('$serverIp/visualiser/generate'); - final response = await http.post( - url, - headers: {'Content-Type': 'application/json'}, - body: jsonEncode({ - 'topic': widget.topic, - 'variables': widget.variables, - }), - ); - - if (response.statusCode == 200) { - final data = jsonDecode(response.body); - final templateJson = data['template'] as Map; - final template = VisualTemplate.fromJson(templateJson); - - final templateId = template.templateId.toLowerCase(); - Widget? newWidget; - - final p = template.parameters; - double getVal(String key) => (p[key]?.value != null) ? (p[key]?.value as num).toDouble() : 0.0; - - if (templateId.contains('projectile')) { - newWidget = ProjectileMotionWidget( - U: getVal('U'), - theta: getVal('theta'), - g: getVal('g'), - ); - } else if (templateId.contains('free') || templateId.contains('fall')) { - newWidget = FreeFallWidget( - h: getVal('h'), - g: getVal('g'), - ); - } else if (templateId.contains('shm')) { - newWidget = SHMWidget( - A: getVal('A'), - m: getVal('m'), - k: getVal('k'), - ); - } else if (templateId.contains('kinematics')) { - newWidget = KinematicsWidget( - u: getVal('u'), - a: getVal('a'), - tMax: getVal('t_max'), - ); - } else if (templateId.contains('optics')) { - newWidget = OpticsWidget( - f: getVal('f'), - u: getVal('u'), - h_o: getVal('h_o'), - ); - } - - if (mounted) { - setState(() { - visualiserTemplate = template; - visualiserWidget = newWidget; - loadingVisualiser = false; - }); - } - } else { - if (mounted) setState(() => loadingVisualiser = false); - } - } catch (e, st) { - print("Error loading visualiser: $e\n$st"); - if (mounted) setState(() => loadingVisualiser = false); - } - } - - void _updateVisualiserWidget(String templateId, Map params) { - double getVal(String k) { - final v = params[k]; - if (v is num) return v.toDouble(); - if (v is String) return double.tryParse(v) ?? 0.0; - return 0.0; - } - - Widget? newWidget; - final id = templateId.toLowerCase(); - - if (id.contains("projectile")) { - newWidget = ProjectileMotionWidget( - U: getVal("U"), - theta: getVal("theta"), - g: getVal("g"), - ); - } else if (id.contains("free")) { - newWidget = FreeFallWidget( - h: getVal("h"), - g: getVal("g"), - ); - } else if (id.contains("shm")) { - newWidget = SHMWidget( - A: getVal("A"), - m: getVal("m"), - k: getVal("k"), - ); - } else if (id.contains("kinematics")) { - newWidget = KinematicsWidget( - u: getVal("u"), - a: getVal("a"), - tMax: getVal("t_max"), - ); - } else if (id.contains("optics")) { - newWidget = OpticsWidget( - f: getVal("f"), - u: getVal("u"), - h_o: getVal("h_o"), - ); - } - - if (mounted) setState(() => visualiserWidget = newWidget); - } - - // --------------------------------------------------------- - // UI BUILD - // --------------------------------------------------------- - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final cs = theme.colorScheme; - - final deepBlue = cs.primary; - final primaryColor = cs.primaryContainer; - final cardColor = theme.cardColor; - final background = theme.scaffoldBackgroundColor; - - return DefaultTabController( - length: 2, - child: Scaffold( - backgroundColor: background, - - // --------------------------------------------------------- - // NEW POLISHED APP BAR - // --------------------------------------------------------- - appBar: AppBar( - elevation: 0, - backgroundColor: primaryColor, - iconTheme: IconThemeData(color: deepBlue), - title: Text( - "Scan Result", - style: TextStyle( - color: deepBlue, - fontWeight: FontWeight.w700, - fontSize: 22, - letterSpacing: 0.3, - ), - ), - centerTitle: true, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - bottom: Radius.circular(26), - ), - ), - bottom: PreferredSize( - preferredSize: const Size.fromHeight(65), - child: Padding( - padding: const EdgeInsets.only(bottom: 10, left: 16, right: 16), - child: Container( - height: 48, - decoration: BoxDecoration( - color: deepBlue.withOpacity(0.10), - borderRadius: BorderRadius.circular(30), - ), - child: TabBar( - dividerColor: Colors.transparent, - labelColor: Colors.white, - unselectedLabelColor: deepBlue.withOpacity(0.7), - indicator: BoxDecoration( - borderRadius: BorderRadius.circular(30), - gradient: LinearGradient( - colors: [ - deepBlue, - deepBlue.withOpacity(0.85), - ], - ), - boxShadow: [ - BoxShadow( - color: deepBlue.withOpacity(0.3), - blurRadius: 8, - offset: const Offset(0, 3), - ) - ], - ), - indicatorSize: TabBarIndicatorSize.tab, - labelStyle: const TextStyle( - fontWeight: FontWeight.w700, - fontSize: 15, - ), - unselectedLabelStyle: const TextStyle( - fontWeight: FontWeight.w600, - fontSize: 14, - ), - tabs: const [ - Tab(text: "AI Visualiser"), - Tab(text: "AI Notes"), - ], - ), - ), - ), - ), - ), - - // --------------------------------------------------------- - // BODY - // --------------------------------------------------------- - body: TabBarView( - children: [ - _visualiser(deepBlue, cardColor), - _notes(cardColor, deepBlue), - ], - ), - ), - ); - } - - // --------------------------------------------------------- - // VISUALISER TAB - // --------------------------------------------------------- - Widget _visualiser(Color deepBlue, Color cardColor) { - if (loadingVisualiser) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - CircularProgressIndicator(color: deepBlue), - const SizedBox(height: 12), - Text("Generating visualiser...", style: TextStyle(color: deepBlue)), - ], - ), - ); - } - - if (visualiserWidget == null) { - return Center( - child: Padding( - padding: const EdgeInsets.all(20), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.bolt_outlined, size: 64, color: deepBlue.withOpacity(0.5)), - const SizedBox(height: 12), - Text("No visualiser available", style: TextStyle(color: deepBlue, fontWeight: FontWeight.bold)), - const SizedBox(height: 8), - Text("The server didn't return a supported visualiser for the topic.", style: TextStyle(color: deepBlue.withOpacity(0.7)), textAlign: TextAlign.center), - const SizedBox(height: 16), - ElevatedButton( - onPressed: _loadVisualiser, - child: const Text("Retry"), - ), - ], - ), - ), - ); - } - - // If we have a visualiser widget, show it inside a card and also list parameters (if present) - return SingleChildScrollView( - padding: const EdgeInsets.all(16), - child: Column( - children: [ - Container( - width: double.infinity, - padding: const EdgeInsets.all(14), - decoration: BoxDecoration( - color: cardColor, - borderRadius: BorderRadius.circular(18), - ), - child: Column( - children: [ - Text("Visualiser", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: deepBlue)), - const SizedBox(height: 12), - SizedBox( - height: 280, - child: Center(child: visualiserWidget), - ), - ], - ), - ), - const SizedBox(height: 16), - if (visualiserTemplate != null) - Container( - width: double.infinity, - padding: const EdgeInsets.all(14), - decoration: BoxDecoration( - color: cardColor, - borderRadius: BorderRadius.circular(18), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text("Parameters", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: deepBlue)), - const SizedBox(height: 12), - ...visualiserTemplate!.parameters.entries.map((e) { - final param = e.value; - final displayVal = param.value?.toString() ?? '—'; - return Padding( - padding: const EdgeInsets.symmetric(vertical: 6), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(param.name ?? e.key, style: TextStyle(color: deepBlue)), - Text(displayVal, style: TextStyle(color: deepBlue.withOpacity(0.8))), - ], - ), - ); - }).toList() - ], - ), - ), - ], - ), - ); - } - - // --------------------------------------------------------- - // NOTES - // --------------------------------------------------------- - Widget _notes(Color cardColor, Color deepBlue) { - // Debug logging - print("📝 Building notes UI"); - print("📝 Notes JSON keys: ${widget.notesJson.keys.toList()}"); - print("📝 Notes JSON: ${widget.notesJson}"); - - // Check for error in response - if (widget.notesJson.containsKey("error")) { - return Center( - child: Padding( - padding: const EdgeInsets.all(24), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.error_outline, size: 64, color: deepBlue.withOpacity(0.5)), - const SizedBox(height: 16), - Text( - "Failed to load notes", - style: TextStyle( - color: deepBlue, - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 8), - Text( - widget.notesJson["error"].toString(), - style: TextStyle(color: deepBlue.withOpacity(0.7), fontSize: 14), - textAlign: TextAlign.center, - ), - ], - ), - ), - ); - } - - // Check for empty notes - if (widget.notesJson.isEmpty) { - return Center( - child: Padding( - padding: const EdgeInsets.all(24), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.note_outlined, size: 64, color: deepBlue.withOpacity(0.5)), - const SizedBox(height: 16), - Text( - "No notes available", - style: TextStyle( - color: deepBlue, - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 8), - Text( - "Notes generation may have failed", - style: TextStyle(color: deepBlue.withOpacity(0.7), fontSize: 14), - textAlign: TextAlign.center, - ), - ], - ), - ), - ); - } - - return SingleChildScrollView( - padding: const EdgeInsets.all(16), - child: Column( - children: widget.notesJson.entries.map((entry) { - final key = entry.key; - - // ensure expanded map has key - if (!expanded.containsKey(key)) expanded[key] = false; - - return _expandableCard( - title: _formatKey(key), - expanded: expanded[key]!, - onTap: () => setState(() => expanded[key] = !expanded[key]!), - child: _buildContent(entry.value, deepBlue), - cardColor: cardColor, - deepBlue: deepBlue, - ); - }).toList(), - ), - ); - } - - Widget _expandableCard({ - required String title, - required bool expanded, - required VoidCallback onTap, - required Widget child, - required Color cardColor, - required Color deepBlue, - }) { - return AnimatedContainer( - duration: const Duration(milliseconds: 260), - margin: const EdgeInsets.only(bottom: 16), - decoration: BoxDecoration( - color: cardColor, - borderRadius: BorderRadius.circular(18), - ), - child: Column( - children: [ - InkWell( - borderRadius: BorderRadius.circular(18), - onTap: onTap, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - title, - style: TextStyle( - fontSize: 18, - color: deepBlue, - fontWeight: FontWeight.w700, - ), - ), - Icon( - expanded ? Icons.keyboard_arrow_up_rounded : Icons.keyboard_arrow_down_rounded, - size: 30, - color: deepBlue, - ), - ], - ), - ), - ), - AnimatedCrossFade( - duration: const Duration(milliseconds: 260), - crossFadeState: expanded ? CrossFadeState.showSecond : CrossFadeState.showFirst, - firstChild: const SizedBox.shrink(), - secondChild: Padding(padding: const EdgeInsets.all(14), child: child), - ), - ], - ), - ); - } - - Widget _buildContent(dynamic value, Color deepBlue) { - if (value is String) { - return Text(value, style: TextStyle(fontSize: 15, color: deepBlue)); - } - - if (value is List) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: value - .map((v) => Padding( - padding: const EdgeInsets.only(bottom: 8), - child: Text("• $v", style: TextStyle(fontSize: 15, color: deepBlue)), - )) - .toList(), - ); - } - - if (value is Map) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: value.entries - .map((e) => Padding( - padding: const EdgeInsets.only(bottom: 8), - child: Text("${e.key}: ${e.value}", style: TextStyle(fontSize: 15, color: deepBlue)), - )) - .toList(), - ); - } - - return const Text("Unsupported format"); - } - - String _formatKey(String raw) { - if (raw.isEmpty) return ""; - final cleaned = raw.replaceAll("_", " ").trim(); - return cleaned.replaceFirst(cleaned[0], cleaned[0].toUpperCase()); - } -} - - - -//----------------------- -import 'dart:convert'; -import 'package:flutter/material.dart'; -import 'package:http/http.dart' as http; - -import 'package:stemly_app/visualiser/kinematics_component.dart'; -import 'package:stemly_app/visualiser/optics_component.dart'; -import '../visualiser/projectile_motion.dart'; -import '../visualiser/free_fall_component.dart'; -import '../visualiser/shm_component.dart'; -import '../visualiser/visualiser_models.dart'; - -class ScanResultScreen extends StatefulWidget { - final String topic; - final List variables; - final Map notesJson; - final String imagePath; - - const ScanResultScreen({ - super.key, - required this.topic, - required this.variables, - required this.notesJson, - required this.imagePath, - }); - - @override - State createState() => _ScanResultScreenState(); -} - -class _ScanResultScreenState extends State { - final Map expanded = {}; - - VisualTemplate? visualiserTemplate; - Widget? visualiserWidget; - bool loadingVisualiser = true; - - final String serverIp = "http://10.0.2.2:8000"; - - @override - void initState() { - super.initState(); - _loadVisualiser(); - } - - // --------------------------------------------------------- - // LOAD VISUALISER - // --------------------------------------------------------- - Future _loadVisualiser() async { - setState(() => loadingVisualiser = true); - - try { - final res = await http.post( - Uri.parse("$serverIp/visualiser/generate"), - headers: {"Content-Type": "application/json"}, - body: jsonEncode({ - "topic": widget.topic, - "variables": widget.variables, - }), - ); - - if (res.statusCode == 200) { - final data = jsonDecode(res.body); - final template = VisualTemplate.fromJson(data["template"]); - - setState(() { - visualiserTemplate = template; - visualiserWidget = _buildVisualiser(template); - loadingVisualiser = false; - }); - } - } catch (e) { - setState(() => loadingVisualiser = false); - } - } - - Widget? _buildVisualiser(VisualTemplate template) { - final p = template.parameters; - double get(String k) => (p[k]?.value is num) ? (p[k]!.value as num).toDouble() : 0.0; - - final id = template.templateId.toLowerCase(); - - if (id.contains("projectile")) { - return ProjectileMotionWidget( - U: get("U"), - theta: get("theta"), - g: get("g"), - ); - } - if (id.contains("free")) { - return FreeFallWidget( - h: get("h"), - g: get("g"), - ); - } - if (id.contains("shm")) { - return SHMWidget( - A: get("A"), - m: get("m"), - k: get("k"), - ); - } - if (id.contains("kinematics")) { - return KinematicsWidget( - u: get("u"), - a: get("a"), - tMax: get("t_max"), - ); - } - if (id.contains("optics")) { - return OpticsWidget( - f: get("f"), - u: get("u"), - h_o: get("h_o"), - ); - } - - return const Text("Unsupported visualiser"); - } - - // --------------------------------------------------------- - // UI - // --------------------------------------------------------- - @override - Widget build(BuildContext context) { - final cs = Theme.of(context).colorScheme; - - return DefaultTabController( - length: 2, - child: Scaffold( - appBar: AppBar( - backgroundColor: cs.primaryContainer, - centerTitle: true, - title: Text("Scan Result", - style: TextStyle( - color: cs.primary, fontWeight: FontWeight.bold, fontSize: 22)), - bottom: TabBar( - indicatorColor: cs.primary, - labelColor: cs.primary, - tabs: const [ - Tab(text: "AI Visualiser"), - Tab(text: "AI Notes"), - ], - ), - ), - body: TabBarView( - children: [ - _visualiserTab(cs.primary), - _notesTab(cs.primary), - ], - ), - ), - ); - } - - // --------------------------------------------------------- - // VISUALISER TAB - // --------------------------------------------------------- - Widget _visualiserTab(Color deepBlue) { - if (loadingVisualiser) { - return Center(child: CircularProgressIndicator(color: deepBlue)); - } - - if (visualiserWidget == null) { - return Center( - child: Text("No visualiser available", style: TextStyle(color: deepBlue)), - ); - } - - return SingleChildScrollView( - padding: const EdgeInsets.all(16), - child: Column( - children: [ - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.grey.shade100, - borderRadius: BorderRadius.circular(14), - ), - child: SizedBox(height: 280, child: visualiserWidget), - ), - const SizedBox(height: 16), - if (visualiserTemplate != null) - Container( - padding: const EdgeInsets.all(14), - decoration: BoxDecoration( - color: Colors.grey.shade100, - borderRadius: BorderRadius.circular(12), - ), - child: Column( - children: - visualiserTemplate!.parameters.entries.map((e) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 6), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(e.key, style: TextStyle(color: deepBlue)), - Text(e.value.value.toString(), - style: TextStyle(color: deepBlue)), - ], - ), - ); - }).toList(), - ), - ) - ], - ), - ); - } - - // --------------------------------------------------------- - // NOTES TAB (FIXED!) - // --------------------------------------------------------- - Widget _notesTab(Color deepBlue) { - final notes = widget.notesJson; - - // EMPTY / ERROR - if (notes.isEmpty) { - return Center(child: Text("No notes available", style: TextStyle(color: deepBlue))); - } - if (notes.containsKey("error")) { - return Center(child: Text(notes["error"], style: TextStyle(color: deepBlue))); - } - - // NORMAL - return SingleChildScrollView( - padding: const EdgeInsets.all(14), - child: Column( - children: notes.entries.map((entry) { - expanded.putIfAbsent(entry.key, () => false); - - return _expandSection( - _pretty(entry.key), - expanded[entry.key]!, - () => setState(() => expanded[entry.key] = !expanded[entry.key]!), - _renderJson(entry.value, deepBlue), - deepBlue, - ); - }).toList(), - ), - ); - } - - // --------------------------------------------------------- - // UNIVERSAL JSON RENDERER (Works for ALL notes) - // --------------------------------------------------------- - Widget _renderJson(dynamic value, Color deepBlue) { - if (value == null) return Text("—"); - - if (value is String) { - return Text(value, style: TextStyle(color: deepBlue, fontSize: 15)); - } - - if (value is num) { - return Text(value.toString(), style: TextStyle(color: deepBlue)); - } - - if (value is List) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: value.map((v) { - return Padding( - padding: const EdgeInsets.only(bottom: 6), - child: _renderJson(v, deepBlue), - ); - }).toList(), - ); - } - - if (value is Map) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: value.entries.map((e) { - return Padding( - padding: const EdgeInsets.only(bottom: 6), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text("${_pretty(e.key)}: ", - style: TextStyle( - color: deepBlue, - fontSize: 15, - fontWeight: FontWeight.bold)), - Expanded(child: _renderJson(e.value, deepBlue)), - ], - ), - ); - }).toList(), - ); - } - - return Text(value.toString(), style: TextStyle(color: deepBlue)); - } - - // --------------------------------------------------------- - // EXPANDABLE CARD - // --------------------------------------------------------- - Widget _expandSection( - String title, bool expanded, VoidCallback onTap, Widget child, Color deepBlue) { - return Container( - margin: const EdgeInsets.only(bottom: 12), - decoration: BoxDecoration( - color: Colors.grey.shade100, - borderRadius: BorderRadius.circular(12), - ), - child: Column( - children: [ - ListTile( - onTap: onTap, - title: Text(title, - style: TextStyle( - color: deepBlue, - fontWeight: FontWeight.bold, - fontSize: 16)), - trailing: Icon( - expanded ? Icons.expand_less : Icons.expand_more, - color: deepBlue), - ), - if (expanded) - Padding( - padding: const EdgeInsets.all(14), - child: child, - ) - ], - ), - ); - } - - // Pretty print section titles - String _pretty(String key) { - return key.replaceAll("_", " ").toUpperCase(); - } -}