diff --git a/doc/gsoc/2025/codes/example4.dart b/doc/gsoc/2025/codes/example4.dart new file mode 100644 index 00000000..e2eb5f3d --- /dev/null +++ b/doc/gsoc/2025/codes/example4.dart @@ -0,0 +1,741 @@ +import 'package:flutter/material.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + debugShowCheckedModeBanner: false, + home: Scaffold(body: Center(child: SDUIWidget())), + ); + } +} + +class SDUIWidget extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFF303030), + body: Column( + children: [ + Text( + "Different Dog Breeds", + style: TextStyle( + color: const Color(0xFFFF00), + fontSize: 30.0, + ), + ), + Expanded( + child: GridView.count( + padding: const EdgeInsets.fromLTRB(10, 10, 10, 10), + crossAxisCount: 3, + mainAxisSpacing: 10.0, + crossAxisSpacing: 10.0, + childAspectRatio: 1.0, + children: [ + Card( + color: const Color(0xFFFFFFE0), + child: Expanded( + child: Padding( + padding: const EdgeInsets.fromLTRB(8, 8, 8, 8), + child: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Card( + color: const Color(0xFFCCCAC0), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Name: Affenpinscher", + style: TextStyle( + fontSize: 16.8, + fontWeight: FontWeight.w700, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + Card( + color: const Color(0xFFCCCAC0), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Description: The Affenpinscher is a small and playful breed of dog that was originally bred in Germany for hunting small game. They are intelligent, energetic, and affectionate, and make excellent companion dogs.", + style: TextStyle( + fontSize: 16.8, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + Card( + color: const Color(0xFFCCCAC0), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Life Expectancy: 14 - 16 years", + style: TextStyle( + fontSize: 16.8, + fontWeight: FontWeight.w700, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + Card( + color: const Color(0xFFCCCAC0), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Male Weight: 3 - 5 kg", + style: TextStyle( + fontSize: 16.8, + fontWeight: FontWeight.w700, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + Card( + color: const Color(0xFFCCCAC0), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Female Weight: 3 - 5 kg", + style: TextStyle( + fontSize: 16.8, + fontWeight: FontWeight.w700, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + Card( + color: const Color(0xFFCCCAC0), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Hypoallergenic: true", + style: TextStyle( + fontSize: 16.8, + fontWeight: FontWeight.w700, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + ], + ), + ), + ), + ), + ), + Card( + color: const Color(0xFF90EE90), + child: Expanded( + child: Padding( + padding: const EdgeInsets.fromLTRB(8, 8, 8, 8), + child: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Card( + color: const Color(0xFF75C475), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Name: Afghan Hound", + style: TextStyle( + fontSize: 16.8, + fontWeight: FontWeight.w700, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + Card( + color: const Color(0xFF75C475), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Description: The Afghan Hound is a large and elegant breed of dog that was originally bred in Afghanistan for hunting small game. They are intelligent, independent, and athletic, and make excellent companion dogs.", + style: TextStyle( + fontSize: 16.8, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + Card( + color: const Color(0xFF75C475), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Life Expectancy: 12 - 14 years", + style: TextStyle( + fontSize: 16.8, + fontWeight: FontWeight.w700, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + Card( + color: const Color(0xFF75C475), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Male Weight: 23 - 27 kg", + style: TextStyle( + fontSize: 16.8, + fontWeight: FontWeight.w700, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + Card( + color: const Color(0xFF75C475), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Female Weight: 20 - 25 kg", + style: TextStyle( + fontSize: 16.8, + fontWeight: FontWeight.w700, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + Card( + color: const Color(0xFF75C475), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Hypoallergenic: false", + style: TextStyle( + fontSize: 16.8, + fontWeight: FontWeight.w700, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + ], + ), + ), + ), + ), + ), + Card( + color: const Color(0xFFADD8E6), + child: Expanded( + child: Padding( + padding: const EdgeInsets.fromLTRB(8, 8, 8, 8), + child: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Card( + color: const Color(0xFF95BDCC), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Name: Akita", + style: TextStyle( + fontSize: 16.8, + fontWeight: FontWeight.w700, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + Card( + color: const Color(0xFF95BDCC), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Description: The Akita is a large, muscular dog breed that originated in Japan. They are known for their loyalty and courage.", + style: TextStyle( + fontSize: 16.8, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + Card( + color: const Color(0xFF95BDCC), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Life Expectancy: 10 - 12 years", + style: TextStyle( + fontSize: 16.8, + fontWeight: FontWeight.w700, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + Card( + color: const Color(0xFF95BDCC), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Male Weight: 35 - 60 kg", + style: TextStyle( + fontSize: 16.8, + fontWeight: FontWeight.w700, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + Card( + color: const Color(0xFF95BDCC), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Female Weight: 35 - 50 kg", + style: TextStyle( + fontSize: 16.8, + fontWeight: FontWeight.w700, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + Card( + color: const Color(0xFF95BDCC), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Hypoallergenic: false", + style: TextStyle( + fontSize: 16.8, + fontWeight: FontWeight.w700, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + ], + ), + ), + ), + ), + ), + Card( + color: const Color(0xFFE6E6FA), + child: Expanded( + child: Padding( + padding: const EdgeInsets.fromLTRB(8, 8, 8, 8), + child: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Card( + color: const Color(0xFFD0D0E0), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Name: Alaskan Klee Kai", + style: TextStyle( + fontSize: 16.8, + fontWeight: FontWeight.w700, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + Card( + color: const Color(0xFFD0D0E0), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Description: The Alaskan Klee Kai is a small to medium-sized breed of dog that was developed in Alaska in the 1970s. It is an active and intelligent breed that is loyal and friendly. The Alaskan Klee Kai stands between 13-17 inches at the shoulder and has a double-coat that can come in various colors and patterns.", + style: TextStyle( + fontSize: 16.8, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + Card( + color: const Color(0xFFD0D0E0), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Life Expectancy: 12 - 15 years", + style: TextStyle( + fontSize: 16.8, + fontWeight: FontWeight.w700, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + Card( + color: const Color(0xFFD0D0E0), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Male Weight: 6 - 7 kg", + style: TextStyle( + fontSize: 16.8, + fontWeight: FontWeight.w700, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + Card( + color: const Color(0xFFD0D0E0), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Female Weight: 6 - 7 kg", + style: TextStyle( + fontSize: 16.8, + fontWeight: FontWeight.w700, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + Card( + color: const Color(0xFFD0D0E0), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Hypoallergenic: false", + style: TextStyle( + fontSize: 16.8, + fontWeight: FontWeight.w700, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + ], + ), + ), + ), + ), + ), + Card( + color: const Color(0xFFF08080), + child: Expanded( + child: Padding( + padding: const EdgeInsets.fromLTRB(8, 8, 8, 8), + child: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Card( + color: const Color(0xFFD16767), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Name: Alaskan Malamute", + style: TextStyle( + fontSize: 16.8, + fontWeight: FontWeight.w700, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + Card( + color: const Color(0xFFD16767), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Description: The Alaskan Malamute is a large and powerful sled dog from Alaska. They are strong and hardworking, yet friendly and loyal. Alaskan Malamutes have a thick, double coat that can be any color. They are active and require plenty of exercise and mental stimulation to stay healthy and happy.", + style: TextStyle( + fontSize: 16.8, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + Card( + color: const Color(0xFFD16767), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Life Expectancy: 12 - 15 years", + style: TextStyle( + fontSize: 16.8, + fontWeight: FontWeight.w700, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + Card( + color: const Color(0xFFD16767), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Male Weight: 34 - 39 kg", + style: TextStyle( + fontSize: 16.8, + fontWeight: FontWeight.w700, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + Card( + color: const Color(0xFFD16767), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Female Weight: 25 - 34 kg", + style: TextStyle( + fontSize: 16.8, + fontWeight: FontWeight.w700, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + Card( + color: const Color(0xFFD16767), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Hypoallergenic: false", + style: TextStyle( + fontSize: 16.8, + fontWeight: FontWeight.w700, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + ], + ), + ), + ), + ), + ), + Card( + color: const Color(0xFFFFFFE0), + child: Expanded( + child: Padding( + padding: const EdgeInsets.fromLTRB(8, 8, 8, 8), + child: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Card( + color: const Color(0xFFCCCAC0), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Name: American Bulldog", + style: TextStyle( + fontSize: 16.8, + fontWeight: FontWeight.w700, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + Card( + color: const Color(0xFFCCCAC0), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Description: The American Bulldog is a large and powerful breed of dog that was originally bred in the United States for working on farms. They are intelligent, loyal, and protective, and make excellent guard dogs.", + style: TextStyle( + fontSize: 16.8, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + Card( + color: const Color(0xFFCCCAC0), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Life Expectancy: 12 - 14 years", + style: TextStyle( + fontSize: 16.8, + fontWeight: FontWeight.w700, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + Card( + color: const Color(0xFFCCCAC0), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Male Weight: 25 - 50 kg", + style: TextStyle( + fontSize: 16.8, + fontWeight: FontWeight.w700, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + Card( + color: const Color(0xFFCCCAC0), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Female Weight: 25 - 45 kg", + style: TextStyle( + fontSize: 16.8, + fontWeight: FontWeight.w700, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + Card( + color: const Color(0xFFCCCAC0), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Hypoallergenic: false", + style: TextStyle( + fontSize: 16.8, + fontWeight: FontWeight.w700, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + ], + ), + ), + ), + ), + ), + Card( + color: const Color(0xFF90EE90), + child: Expanded( + child: Padding( + padding: const EdgeInsets.fromLTRB(8, 8, 8, 8), + child: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Card( + color: const Color(0xFF75C475), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Name: American English Coonhound", + style: TextStyle( + fontSize: 16.8, + fontWeight: FontWeight.w700, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + Card( + color: const Color(0xFF75C475), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Description: The American English Coonhound is a large and athletic breed of dog that was originally bred in the United States for hunting raccoons. They are intelligent, energetic, and determined, and make excellent hunting dogs.", + style: TextStyle( + fontSize: 16.8, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + Card( + color: const Color(0xFF75C475), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Life Expectancy: 12 - 14 years", + style: TextStyle( + fontSize: 16.8, + fontWeight: FontWeight.w700, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + Card( + color: const Color(0xFF75C475), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Male Weight: 20 - 30 kg", + style: TextStyle( + fontSize: 16.8, + fontWeight: FontWeight.w700, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + Card( + color: const Color(0xFF75C475), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Female Weight: 20 - 30 kg", + style: TextStyle( + fontSize: 16.8, + fontWeight: FontWeight.w700, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + Card( + color: const Color(0xFF75C475), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + child: Text( + "Hypoallergenic: false", + style: TextStyle( + fontSize: 16.8, + fontWeight: FontWeight.w700, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ), + ], + ), + ), + ), + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/doc/gsoc/2025/codes/example5.dart b/doc/gsoc/2025/codes/example5.dart new file mode 100644 index 00000000..6a114ff6 --- /dev/null +++ b/doc/gsoc/2025/codes/example5.dart @@ -0,0 +1,396 @@ +import 'package:flutter/material.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + debugShowCheckedModeBanner: false, + home: Scaffold(body: Center(child: SDUIWidget())), + ); + } +} + +class SDUIWidget extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFFFFFFFF), + body: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Card( + color: const Color(0xFFE6F7FF), + margin: const EdgeInsets.fromLTRB(10, 10, 10, 10), + elevation: 3, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(10, 10, 10, 0), + child: Text( + "Word: flutter", + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w700, + color: const Color(0xFF00008B), + ), + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(10, 5, 10, 10), + child: Text( + "Phonetic: /ˈflʌtə/", + style: TextStyle( + fontSize: 16, + color: const Color(0xFF777777), + ), + ), + ), + Card( + color: const Color(0xFF676f8f), + margin: const EdgeInsets.fromLTRB(10, 5, 10, 10), + elevation: 2, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(10, 10, 10, 0), + child: Text( + "Part of Speech: noun", + style: TextStyle( + fontWeight: FontWeight.w700, + color: const Color(0xFFFFFF00), + ), + ), + ), + Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Card( + color: const Color(0xFF242838), + margin: const EdgeInsets.fromLTRB(10, 5, 10, 10), + elevation: 1, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB( + 10, 10, 10, 0), + child: Text( + "Definition: The act of fluttering; quick and irregular motion.", + style: TextStyle( + color: const Color(0xFFFFFFFF), + ), + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB( + 10, 5, 10, 10), + child: Text( + "Example: the flutter of a fan", + style: TextStyle( + fontStyle: FontStyle.italic, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ], + ), + ), + Card( + color: const Color(0xFF242838), + margin: const EdgeInsets.fromLTRB(10, 5, 10, 10), + elevation: 1, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(10.0), + child: Text( + "Definition: A state of agitation.", + style: TextStyle( + color: const Color(0xFFFFFFFF), + ), + ), + ), + ], + ), + ), + Card( + color: const Color(0xFF242838), + margin: const EdgeInsets.fromLTRB(10, 5, 10, 10), + elevation: 1, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(10.0), + child: Text( + "Definition: An abnormal rapid pulsation of the heart.", + style: TextStyle( + color: const Color(0xFFFFFFFF), + ), + ), + ), + ], + ), + ), + Card( + color: const Color(0xFF242838), + margin: const EdgeInsets.fromLTRB(10, 5, 10, 10), + elevation: 1, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(10.0), + child: Text( + "Definition: A small bet or risky investment.", + style: TextStyle( + color: const Color(0xFFFFFFFF), + ), + ), + ), + ], + ), + ), + Card( + color: const Color(0xFF242838), + margin: const EdgeInsets.fromLTRB(10, 5, 10, 10), + elevation: 1, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(10.0), + child: Text( + "Definition: A hasty game of cards or similar.", + style: TextStyle( + color: const Color(0xFFFFFFFF), + ), + ), + ), + ], + ), + ), + Card( + color: const Color(0xFF242838), + margin: const EdgeInsets.fromLTRB(10, 5, 10, 10), + elevation: 1, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(10.0), + child: Text( + "Definition: (audio) The rapid variation of signal parameters, such as amplitude, phase, and frequency.", + style: TextStyle( + color: const Color(0xFFFFFFFF), + ), + ), + ), + ], + ), + ), + ], + ), + ], + ), + ), + Card( + color: const Color(0xFF676f8f), + margin: const EdgeInsets.fromLTRB(10, 5, 10, 10), + elevation: 2, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(10, 10, 10, 0), + child: Text( + "Part of Speech: verb", + style: TextStyle( + fontWeight: FontWeight.w700, + color: const Color(0xFFFFFF00), + ), + ), + ), + Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Card( + color: const Color(0xFF242838), + margin: const EdgeInsets.fromLTRB(10, 5, 10, 10), + elevation: 1, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB( + 10, 10, 10, 0), + child: Text( + "Definition: To flap or wave quickly but irregularly.", + style: TextStyle( + color: const Color(0xFFFFFFFF), + ), + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB( + 10, 5, 10, 10), + child: Text( + "Example: flags fluttering in the wind", + style: TextStyle( + fontStyle: FontStyle.italic, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ], + ), + ), + Card( + color: const Color(0xFF242838), + margin: const EdgeInsets.fromLTRB(10, 5, 10, 10), + elevation: 1, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(10.0), + child: Text( + "Definition: Of a winged animal: to flap the wings without flying; to fly with a light flapping of the wings.", + style: TextStyle( + color: const Color(0xFFFFFFFF), + ), + ), + ), + ], + ), + ), + Card( + color: const Color(0xFF242838), + margin: const EdgeInsets.fromLTRB(10, 5, 10, 10), + elevation: 1, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB( + 10, 10, 10, 0), + child: Text( + "Definition: To cause something to flap.", + style: TextStyle( + color: const Color(0xFFFFFFFF), + ), + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB( + 10, 5, 10, 10), + child: Text( + "Example: A bird flutters its wings.", + style: TextStyle( + fontStyle: FontStyle.italic, + color: const Color(0xFFFFFFFF), + ), + ), + ), + ], + ), + ), + Card( + color: const Color(0xFF242838), + margin: const EdgeInsets.fromLTRB(10, 5, 10, 10), + elevation: 1, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(10.0), + child: Text( + "Definition: To drive into disorder; to throw into confusion.", + style: TextStyle( + color: const Color(0xFFFFFFFF), + ), + ), + ), + ], + ), + ), + Card( + color: const Color(0xFF242838), + margin: const EdgeInsets.fromLTRB(10, 5, 10, 10), + elevation: 1, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(10.0), + child: Text( + "Definition: To be in a state of agitation or uncertainty.", + style: TextStyle( + color: const Color(0xFFFFFFFF), + ), + ), + ), + ], + ), + ), + Card( + color: const Color(0xFF242838), + margin: const EdgeInsets.fromLTRB(10, 5, 10, 10), + elevation: 1, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(10.0), + child: Text( + "Definition: To be frivolous.", + style: TextStyle( + color: const Color(0xFFFFFFFF), + ), + ), + ), + ], + ), + ), + ], + ), + ], + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/doc/gsoc/2025/images/aireq1.png b/doc/gsoc/2025/images/aireq1.png new file mode 100644 index 00000000..ee4d72ad Binary files /dev/null and b/doc/gsoc/2025/images/aireq1.png differ diff --git a/doc/gsoc/2025/images/aireq2.png b/doc/gsoc/2025/images/aireq2.png new file mode 100644 index 00000000..77464427 Binary files /dev/null and b/doc/gsoc/2025/images/aireq2.png differ diff --git a/doc/gsoc/2025/images/aiuiexample1.png b/doc/gsoc/2025/images/aiuiexample1.png new file mode 100644 index 00000000..674ac298 Binary files /dev/null and b/doc/gsoc/2025/images/aiuiexample1.png differ diff --git a/doc/gsoc/2025/images/apischema.png b/doc/gsoc/2025/images/apischema.png new file mode 100644 index 00000000..813294a4 Binary files /dev/null and b/doc/gsoc/2025/images/apischema.png differ diff --git a/doc/gsoc/2025/images/bnetlcov.png b/doc/gsoc/2025/images/bnetlcov.png new file mode 100644 index 00000000..0b6133a3 Binary files /dev/null and b/doc/gsoc/2025/images/bnetlcov.png differ diff --git a/doc/gsoc/2025/images/codecovold.png b/doc/gsoc/2025/images/codecovold.png new file mode 100644 index 00000000..789908d6 Binary files /dev/null and b/doc/gsoc/2025/images/codecovold.png differ diff --git a/doc/gsoc/2025/images/exmp2.png b/doc/gsoc/2025/images/exmp2.png new file mode 100644 index 00000000..363da4ca Binary files /dev/null and b/doc/gsoc/2025/images/exmp2.png differ diff --git a/doc/gsoc/2025/images/gencomp.png b/doc/gsoc/2025/images/gencomp.png new file mode 100644 index 00000000..42db9613 Binary files /dev/null and b/doc/gsoc/2025/images/gencomp.png differ diff --git a/doc/gsoc/2025/images/llmarch.png b/doc/gsoc/2025/images/llmarch.png new file mode 100644 index 00000000..f8442003 Binary files /dev/null and b/doc/gsoc/2025/images/llmarch.png differ diff --git a/doc/gsoc/2025/images/modelselector1.png b/doc/gsoc/2025/images/modelselector1.png new file mode 100644 index 00000000..a48fe4b6 Binary files /dev/null and b/doc/gsoc/2025/images/modelselector1.png differ diff --git a/doc/gsoc/2025/images/modelselector2.png b/doc/gsoc/2025/images/modelselector2.png new file mode 100644 index 00000000..a39afd58 Binary files /dev/null and b/doc/gsoc/2025/images/modelselector2.png differ diff --git a/doc/gsoc/2025/images/pincodetable.png b/doc/gsoc/2025/images/pincodetable.png new file mode 100644 index 00000000..f19cfdd7 Binary files /dev/null and b/doc/gsoc/2025/images/pincodetable.png differ diff --git a/doc/gsoc/2025/images/sse_ex1.png b/doc/gsoc/2025/images/sse_ex1.png new file mode 100644 index 00000000..03ea73b6 Binary files /dev/null and b/doc/gsoc/2025/images/sse_ex1.png differ diff --git a/doc/gsoc/2025/images/stacreq.png b/doc/gsoc/2025/images/stacreq.png new file mode 100644 index 00000000..0f16a788 Binary files /dev/null and b/doc/gsoc/2025/images/stacreq.png differ diff --git a/doc/gsoc/2025/images/stacreq2.png b/doc/gsoc/2025/images/stacreq2.png new file mode 100644 index 00000000..931587dd Binary files /dev/null and b/doc/gsoc/2025/images/stacreq2.png differ diff --git a/doc/gsoc/2025/images/toolgen.png b/doc/gsoc/2025/images/toolgen.png new file mode 100644 index 00000000..0da50583 Binary files /dev/null and b/doc/gsoc/2025/images/toolgen.png differ diff --git a/doc/gsoc/2025/manas_hejmadi.md b/doc/gsoc/2025/manas_hejmadi.md index 8b137891..1ab4e56e 100644 --- a/doc/gsoc/2025/manas_hejmadi.md +++ b/doc/gsoc/2025/manas_hejmadi.md @@ -1 +1,709 @@ +# GSoC'25 - AI-Based API Response to Dynamic UI and Tool Generator +> Final report summarizing my contributions to the project as part of GSoC'25 + +## Project Details +1. **Contributor** : Manas M Hejmadi +2. **Mentors** : Ashita P, Ankit M, Ragul Raj M +3. **Organization**: API Dash +4. **Project**: AI-Based API Response to Dynamic UI and Tool Generator + +#### Quick Links +- [GSoC Project Page](https://summerofcode.withgoogle.com/programs/2025/projects/hhUUM8wl) +- [Code Repository](https://github.com/foss42/apidash) +- [Discussion Logs](https://github.com/foss42/apidash/discussions/852) + +## Project Description + +The primary objective of this project was to extend the API Dash client with new generative AI capabilities that go far beyond the scope of traditional API testing clients. This will position API Dash as an OpenSource AI-Native API Testing client. + +Our initial vision was to develop an AI-powered agent capable of transforming raw API responses into structured UI schemas and fully functional UI components that could be directly exported and used in frontend applications. Additionally, we wanted to enable dynamic customization of UI components through natural language prompts, allowing developers to customise the design and layout according to their personal preference. This UI code could then be exported and used directly in their Flutter projects. + +As mentioned in the project proposal, we were also aiming to create a one-click `API Request to Tool` generation pipeline, allowing external AI agents to independently interact with APIs. This is a crucial requirement for modern agentic workflows and the idea was that API Dash must be ready to serve these needs. + +However, during the planning phase it became clear that these ambitious features required strong foundational infrastructure to work at a production level. Under the guidance of my mentors, we identified and implemented several core architectural improvements, such as: + +- Refactoring the networking layer into a modular, standalone package to enhance testability and maintainability. +- Adding streaming support via Server-Sent Events (SSE) to enable real-time AI interactions. +- Introducing AI request handling and a dedicated AI primitives package. This ensures that any future API Dash feature that would need generative ai, can directly import this primitives package instead of implementing everything again. this saves both time and effort. + +All in all, these completion of these improvements will establish API Dash as a modern, industry ready platform for developers and AI-driven workflows alike. + + +## Feature Description + +### `better_networking` package creation & project-wide refactor + +`Package Link`: https://pub.dev/packages/better_networking + +`Associated Pull Request`: [#857](https://github.com/foss42/apidash/pull/857) + +Initially, the entire networking constructs that API Dash relied on was fully written inside a module named `apidash_core`. +The networking code was fairly advanced including support for GraphQL, request cancellations and a lot of other good features. However, as it was tightly coupled with API Dash, we were unable to allow the rest of the flutter developer community to use these features. We believe in giving back to the open source community whenever we can and hence the mentors and I decided to refactor everything into a new package. +During discussions, I came up with the name `better_networking` and we envisioned it to be the go-to package for everything related to networking for a flutter application. + +This is an example of how better_networking simplifies request handling + + +```dart +final model = HttpRequestModel( + url: 'https://api.example.com/data', + method: HTTPVerb.post, + headers: [ + NameValueModel(name: 'Authorization', value: 'Bearer '), + ], + body: '{"key": "value"}', +); + +//Sending HTTP Requests +final (resp, duration, err) = await sendHttpRequest( + 'unique-request-id', + APIType.rest, + model, +); + +// To cancel the request +cancelHttpRequest('unique-request-id'); +``` + +This proved to be a great decision, as we were able to separate it completely, publish it on [pub.dev](https://pub.dev/packages/better_networking), and achieve over 95% code coverage through isolated testing. + +Code coverage before refactor: +![Code Coverage Report](./images/codecovold.png) + +Code coverage after Refactor: +![Code Coverage Report](./images/bnetlcov.png) + +--- + +### Added SSE and Streaming Support to the Client +`Associated Pull Request`: [#861](https://github.com/foss42/apidash/pull/861) + +![Code Coverage Report](./images/sse_ex1.png) + +SSE Support was a long pending [issue](https://github.com/foss42/apidash/issues/116) (since 2024). Once I completed the initial version of better_networking, the mentors asked me to look if SSE support can be added within the package and by extension into API Dash, as AI responses are usually transmitted in this format. After doing some research and a review into the existing PRs by other contributors for this feature, I noticed that everyone created new Request and Response Models for SSE in code. + +However, I did not agree with this approach as SSE is just a different content-type is not a fundamentally separate request type like GraphQL. +To demonstrate this, I wrote up a quick demo with SSE baked into the existing API Dash foundations. + +This new mechanism is very simple and elegant. Basically, every request in API Dash is executed in streaming mode using `StreamedResponse` in dart. If the response headers specify a content-type marked as streaming, the listener remains active and statefully saves all incoming values into the sseOutput attribute of the response model. If the content-type does not match any supported streaming type, the listener terminates and the output is returned immediately. In this way, the existing request/response model can handle both Streaming and Normal HTTP Requests + +This is an example of how I rewrote the original implementation of `sendHttpRequest` in terms of this new SSE handler +```dart +Future<(HttpResponse?, Duration?, String?)> sendHttpRequest( + String requestId, + APIType apiType, + HttpRequestModel requestModel, { + SupportedUriSchemes defaultUriScheme = kDefaultUriScheme, + bool noSSL = false, +}) async { + final stream = await streamHttpRequest( + requestId, + apiType, + requestModel, + defaultUriScheme: defaultUriScheme, + noSSL: noSSL, + ); + final output = await stream.first; + return (output?.$2, output?.$3, output?.$4); +} +``` +The mentors were impressed with this approach as it was far more maintainable and sensible than creating new models specifically for SSE. +This way, everything stays unified and we reduce the amount of duplication + +--- + +### Added Agents and AI Requests Support + +`Associated Pull Request`: [#870](https://github.com/foss42/apidash/pull/870) + +`Package Link`: https://pub.dev/packages/genai + +Since my project involved sending AI API requests, the next step involved adding support for AI requests and an interface for building agentic systems in Dart & Flutter. Hence, I started developing a comprehensive AI Requests feature. + +The user initiates a new request by selecting “AI”, then chooses a model and provides the required credentials through the Authorization tab. The request can be further configured by specifying system and user prompts and adjusting parameters such as `temperature`, `topP`, and `streaming or non-streaming mode`. Upon submission, the response is generated and presented either as cleaned plaintext/Markdown or in raw format, based on the user’s selection. + + +![AI Requests](./images/aireq1.png) +![AI Requests](./images/aireq2.png) + +My initial implementation used tightly coupled LLM providers (e.g., gemini, openai) with specific models (e.g., gemini-2.0-flash) through hardcoded enums. These enums were directly referenced in code, which on closer review proved unsustainable. Given the rapid pace of innovation in LLMs, models become obsolete quickly, and maintaining hardcoded enums would require frequent code changes and was looking quite impractical. +Furthermore, using hardcoded enums prevents runtime dynamic loading, restricting users to only the models we explicitly provide. This limits flexibility and creates a poor experience, especially for advanced users who may need access to less common or custom models. + +To address this, we adopted a remote model fetch system, where model identifiers are stored in a `models.json` file within the public API Dash repository. Clients fetch this file at runtime, enabling over-the-air updates to model availability. In addition, we added support for custom model identifiers directly within the ModelSelector, giving users full flexibility to configure their own models. + +Currently, we support several standard providers—such as Google Gemini, OpenAI, Anthropic, and Ollama—which offers a strong baseline of options while still allowing advanced customization. + +![LLM Provider Selector](./images/modelselector1.png) + +The AI Requests feature is built on top of the foundational genai package, which serves as the core layer for all AI-related functionality within API Dash. +This package provides the complete set of API callers, methods, and formatters required to abstract away the complexities of interacting with AI tool APIs. By exposing a generalized interface across multiple providers, it eliminates the need to handle provider-specific details directly. +As a result, developers can easily build features that leverage generative AI without worrying about low-level implementation details—leaving the intricacies of API communication and formatting to the genai package. + +Example of simplified usage (model-agnostic, works with any LLM out of the box) +```dart +final request = AIRequestModel( + modelApiProvider: ModelAPIProvider.gemini, // or openai, anthropic, etc. + model: "gemini-2.0-flash", + apiKey: "", + url: kGeminiUrl, + systemPrompt: "You are a helpful assistant.", + userPrompt: "Explain quantum entanglement simply.", + stream: false, // set true for streaming +); +await callGenerativeModel( + request, + onAnswer: (ans) => print("AI Output: $ans"), + onError: (err) => print("Error: $err"), +); +``` + +#### Agentic Infrastructure + +![Agentic Infrastructure](./images/llmarch.png) + +When developing AI-powered features in any application, the process typically involves steps such as system prompting, data validation, and output formatting. However, repeating this workflow for multiple features while taking care of errors and retry logic quickly becomes very cumbersom. To simplify this, we designed a well-defined architecture for building AI agents directly within code. + +The core idea is straightforward: an AI agent in API Dash is simply a Dart file containing a class that extends the base class `APIDashAIAgent`, defined as: +```dart +abstract class AIAgent { + String get agentName; + String getSystemPrompt(); + Future validator(String aiResponse); + Future outputFormatter(String validatedResponse); +} +``` +This base class provides the necessary hooks for implementing an agent. Developers can either rely on the default implementations or override these handlers with custom logic. The result is a fully abstracted, self-contained agent that can be invoked seamlessly from within the application. + +These agents operate within an orchestrator and governor framework that manages everything behind the scenes. This design ensures that developers only need to invoke the agent, while background processes handle concerns such as automatic retries, exponential backoff, and error recovery seamlessly. This saves a lot of time and effort and allows developers to spend more time on improving their actual feature implementation. + + +#### Sample Agent Code + +```dart +//simple_func_agent.dart + +class SimpleFuncGenerator extends AIAgent { + @override + String get agentName => 'SIMPLE_FUNCGEN'; + + @override + String getSystemPrompt() { + return """you are a programming language function generator. + your only task is to take whatever requirement is provided and convert + it into a valid function named func in the provided programming language + + LANGUAGE: :LANG: + REQUIREMENT: :REQUIREMENT: +"""; //suports templating via :: + } + + @override + Future validator(String aiResponse) async { + return aiResponse.contains("func"); + } + + @override + Future outputFormatter(String validatedResponse) async { + validatedResponse = validatedResponse + .replaceAll(RegExp(r'```[a-zA-Z]*\n?'), '') + .replaceAll('```', ''); + return { + 'FUNC': validatedResponse, + }; + } +} + +//Calling an agent + final res = await APIDashAgentCaller.instance.call( + SimpleFuncGenerator(), + ref: ref, + input: AgentInputs(variables: { + 'REQUIREMENT': 'take the median of the given array', + 'TARGET_LANGUAGE': 'python3', + }), + ); + +``` + +--- + +### Created the API Tool Generator +As proposed in my GSoC proposal, I set out to implement an API Tool Call Generator within the application. It consists of an in-app agent that processes API request details and converts them into standardized tool-call code compatible with providers such as OpenAI, Gemini, LangChain, and others. + +This feature is fully built on top of the agentic foundation established by `genai`. Once a user executes an API request and receives a response, they can click “Generate Tool”, which opens a tool generation dialog. Here, the user selects their preferred agent framework along with the target output language (currently Python or Node.js). The client then consolidates all request details, sends them to an LLM, and generates the corresponding function callers. These are subsequently integrated into a predefined, well-researched API Tool template, ensuring reliability and consistency. + +![Tool Generation](./images/toolgen.png) + +Here's the generated tool code +```python +import requests +def func(): + """ + Retrieves a list of users from the reqres.in API. + """ + url = "https://reqres.in/api/users" + headers = { + "X-Api-Key": "reqres-free-v1" + } + try: + response = requests.get(url, headers=headers) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + print(f"Request failed: {e}") + return None + +api_tool = { + "function": func, + "definition": { + "name": "GetUsers", + "description": "Retrieves a list of users from the reqres.in API.", + "parameters": { + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": False + } + } +} + +__all__ = ["api_tool"] +``` + +--- + +### Implemented the API Schema to Flutter UI Generator + +With the foundational infrastructure in place, I was finally getting closer to my GSoC goal of building the AI UI Designer. + +The purpose of this feature is straightforward yet powerful—take API responses and automatically transform them into suitable UI components, while also providing the ability to modify the design through natural language instructions. Additionally, the generated UI can be exported as Flutter code, enabling seamless integration into frontend applications. + +A Proof of Concept (PoC) for this functionality had already been demonstrated during the initial phase of GSoC. The remaining work involved converting the PoC into production-ready code, addressing error handling, improving stability, and ensuring it could scale as a fully integrated feature within API Dash. + +This marks a significant milestone, as the AI UI Designer bridges the gap between raw API responses and usable frontend components—removing boilerplate work and streamlining the developer workflow. + +![API Response](./images/apischema.png) + +With the AI UI Designer, the response returned from the above API can be automatically converted into a Flutter widget. This widget is generated and rendered using the Server-Driven UI (SDUI) approach, powered by the [Stac](https://pub.dev/packages/stac) package. + +This is what the generated component looks like: + +![Generated Widget](./images/gencomp.png) + +The prompts that were used to achieve this final result: + +> 1. Use a ListTile based layout instead of cards and add uniform padding +> 2. Make the page background as light yellow and change the appbar's background color to black and foreground color to white + +### Additional Examples of Generated UI Components + +Here's the Entire Creation Flow + +https://github.com/user-attachments/assets/a4074f28-2aaa-471a-b9bb-623d731b7515 + +#### Example 1 +`GET` https://rickandmortyapi.com/api/character/[1,2,3,4,5,6,7] +```json +[ + { + "id": 1, + "name": "Rick Sanchez", + "status": "Alive", + "species": "Human", + "type": "", + "gender": "Male", + "origin": { + "name": "Earth (C-137)", + "url": "https://rickandmortyapi.com/api/location/1" + }, + "location": { + "name": "Citadel of Ricks", + "url": "https://rickandmortyapi.com/api/location/3" + }, + "image": "https://rickandmortyapi.com/api/character/avatar/1.jpeg", + "episode": [ + "https://rickandmortyapi.com/api/episode/1", + "https://rickandmortyapi.com/api/episode/2", + "https://rickandmortyapi.com/api/episode/3", + ], + "url": "https://rickandmortyapi.com/api/character/1", + "created": "2017-11-04T18:48:46.250Z" + }, + { + "id": 2, + "name": "Morty Smith", + "status": "Alive", + "species": "Human", + "type": "", + "gender": "Male", + "origin": { + "name": "unknown", + "url": "" + }, + "location": { + "name": "Citadel of Ricks", + "url": "https://rickandmortyapi.com/api/location/3" + }, + "image": "https://rickandmortyapi.com/api/character/avatar/2.jpeg", + "url": "https://rickandmortyapi.com/api/character/2", + "created": "2017-11-04T18:50:21.651Z" + }, + ... +] +``` +The Generated Component Preview looks like + +![Generated Widget](./images/aiuiexample1.png) + + +#### Example 2 +`GET` https://api.nasa.gov/planetary/apod?date=&start_date=&end_date=&count=&thumbs +```json +{ + "copyright": "Marina Prol", + "date": "2025-08-30", + "explanation": "A young crescent moon can be hard to see. That's because when the Moon shows its crescent phase (young or old) it can never be far from the Sun in planet Earth's sky. But even though the sky is still bright, a slender sunlit lunar crescent is clearly visible in this early evening skyscape. The telephoto snapshot was captured on August 24, with the Moon very near the western horizon at sunset. Seen in a narrow crescent phase about 1.5 days old, the visible sunlit portion is a mere two percent of the surface of the Moon's familiar nearside. At the Canary Islands Space Centre, a steerable radio dish for communication with spacecraft is tilted in the direction of the two percent Moon. The sunset sky's pastel pinkish coloring is partly due to fine sand and dust from the Sahara Desert blown by the prevailing winds.", + "hdurl": "https://apod.nasa.gov/apod/image/2508/IMG_4081.jpeg", + "media_type": "image", + "service_version": "v1", + "title": "A Two Percent Moon", + "url": "https://apod.nasa.gov/apod/image/2508/IMG_4081_1024.jpeg" +} +``` +Generated UI: + +![Generated Widget](./images/exmp2.png) + +Exported Flutter Code: +```dart +class SDUIWidget extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFF333333), + appBar: AppBar( + backgroundColor: const Color(0xFF222222), + title: const Text( + "Astronomy Picture of the Day", + style: TextStyle( + color: Color(0xFFFFDA63), + ), + ), + ), + body: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Card( + color: const Color(0xFF121212), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Image.network( + "https://apod.nasa.gov/apod/image/2508/IMG_4081_1024.jpeg", + width: 300, + height: 300, + fit: BoxFit.contain, + ), + const Padding( + padding: EdgeInsets.fromLTRB(8, 8, 8, 8), + child: Text( + "A Two Percent Moon", + style: TextStyle( + color: Color(0xFFFFDA63), + fontSize: 24, + ), + textAlign: TextAlign.center, + ), + ), + ], + ), + ), + Card( + color: const Color(0xFF121212), + child: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Padding( + padding: EdgeInsets.fromLTRB(8, 8, 8, 8), + child: Text( + "A young crescent moon can be hard to see. That's because when the Moon shows its crescent phase (young or old) it can never be far from the Sun in planet Earth's sky. But even though the sky is still bright, a slender sunlit lunar crescent is clearly visible in this early evening skyscape. The telephoto snapshot was captured on August 24, with the Moon very near the western horizon at sunset. Seen in a narrow crescent phase about 1.5 days old, the visible sunlit portion is a mere two percent of the surface of the Moon's familiar nearside. At the Canary Islands Space Centre, a steerable radio dish for communication with spacecraft is tilted in the direction of the two percent Moon. The sunset sky's pastel pinkish coloring is partly due to fine sand and dust from the Sahara Desert blown by the prevailing winds.", + style: TextStyle( + color: Color(0xFFFFFACD), + fontSize: 16, + ), + ), + ), + ], + ), + ), + ), + ], + ), + ), + ); + } +} +``` + +#### Example 3 +`GET`: https://api.postalpincode.in/pincode/560078 +```json +[ + { + "Message": "Number of pincode(s) found:4", + "Status": "Success", + "PostOffice": [ + { + "Name": "J P Nagar", + "Description": null, + "BranchType": "Sub Post Office", + "DeliveryStatus": "Delivery", + "Circle": "Karnataka", + "District": "Bangalore", + "Division": "Bangalore South", + "Region": "Bangalore HQ", + "Block": "Bangalore South", + "State": "Karnataka", + "Country": "India", + "Pincode": "560078" + }, + { + "Name": "JP Nagar III Phase", + "Description": null, + "BranchType": "Sub Post Office", + "DeliveryStatus": "Non-Delivery", + "Circle": "Karnataka", + "District": "Bangalore", + "Division": "Bangalore South", + "Region": "Bangalore HQ", + "Block": "Bangalore South", + "State": "Karnataka", + "Country": "India", + "Pincode": "560078" + }, + { + "Name": "Kumaraswamy Layout", + "Description": null, + "BranchType": "Sub Post Office", + "DeliveryStatus": "Non-Delivery", + "Circle": "Karnataka", + "District": "Bangalore", + "Division": "Bangalore South", + "Region": "Bangalore HQ", + "Block": "Bangalore South", + "State": "Karnataka", + "Country": "India", + "Pincode": "560078" + }, + { + "Name": "Yelachenahalli", + "Description": null, + "BranchType": "Sub Post Office", + "DeliveryStatus": "Non-Delivery", + "Circle": "Karnataka", + "District": "Bangalore", + "Division": "Bangalore South", + "Region": "Bangalore HQ", + "Block": "Bangalore South", + "State": "Karnataka", + "Country": "India", + "Pincode": "560078" + } + ] + } +] +``` +Generated UI: + +![](./images/pincodetable.png) + +#### Example 4 +`GET` https://dogapi.dog/api/v2/breeds +```json +{ + "data": [ + { + "id": "667c7359-a739-4f2b-abb4-98867671e375", + "type": "breed", + "attributes": { + "name": "Alaskan Klee Kai", + "description": "The Alaskan Klee Kai is a small to medium-sized breed of dog that was developed in Alaska in the 1970s. It is an active and intelligent breed that is loyal and friendly. The Alaskan Klee Kai stands between 13-17 inches at the shoulder and has a double-coat that can come in various colors and patterns.", + "life": { + "max": 15, + "min": 12 + }, + "male_weight": { + "max": 7, + "min": 6 + }, + "female_weight": { + "max": 7, + "min": 6 + }, + "hypoallergenic": false + }, + "relationships": { + "group": { + "data": { + "id": "8000793f-a1ae-4ec4-8d55-ef83f1f644e5", + "type": "group" + } + } + } + }, + ... + ], + ... +} +``` +Generated UI: + +https://github.com/user-attachments/assets/5ac6794e-c92d-41f3-9e6e-4ffa544827b1 + +Exported Source Code: [Link](./codes/example4.dart) + +#### Example 5 +`GET` https://api.dictionaryapi.dev/api/v2/entries/en/flutter +```json +[ + { + "word": "flutter", + "phonetic": "/ˈflʌtə/", + "phonetics": [ + { + "text": "/ˈflʌtə/", + "audio": "" + }, + ], + "meanings": [ + { + "partOfSpeech": "noun", + "definitions": [ + { + "definition": "The act of fluttering; quick and irregular motion.", + "synonyms": [], + "antonyms": [], + "example": "the flutter of a fan" + }, + { + "definition": "A state of agitation.", + "synonyms": [], + "antonyms": [] + }, + ... + ], + "synonyms": [], + "antonyms": [] + }, + ... + ], + "license": { + "name": "CC BY-SA 3.0", + "url": "https://creativecommons.org/licenses/by-sa/3.0" + }, + "sourceUrls": [ + "https://en.wiktionary.org/wiki/flutter" + ] + } +] +``` +Generated UI: + +https://github.com/user-attachments/assets/673cddf0-8016-48ee-9217-d6f1ed9f826d + +Exported Source Code: [Link](./codes/example5.dart) + +--- + +## Complete Pull Request Report + + + +| Feature | PR | Issue | Status | Comments | +|---|---|---|---|---| +|Proof of Concept & Proposal Doc|[#755](https://github.com/foss42/apidash/pull/755)||Merged|| +|FIX: `` exception|[#780](https://github.com/foss42/apidash/pull/780)|[#782](https://github.com/foss42/apidash/issues/782)|Merged|| +|AI Requests Feature Initial Implementation|[#850](https://github.com/foss42/apidash/pull/850)||Closed|multiple modifications suggested| +|AI Requests Feature Fine tuning|[#856](https://github.com/foss42/apidash/pull/856)||Closed|SSE and Separate Networking layer was deemed necessary before this PR| +|`better_networking` Package Creation|[#857](https://github.com/foss42/apidash/pull/857)||Merged| +`genai` package foundations|[#859](https://github.com/foss42/apidash/pull/859)||Closed|Mentor requested for a new PR after making some changes| +|SSE Feature Foundations|[#860](https://github.com/foss42/apidash/pull/860)||Closed|Mentor requested changes and rebase to main branch| +|SSE & Streaming Support|[#861](https://github.com/foss42/apidash/pull/861)|[#116](https://github.com/foss42/apidash/issues/116)|Merged|| +|`genai` & AI Requests Feature|[#870](https://github.com/foss42/apidash/pull/870)|[#871](https://github.com/foss42/apidash/issues/871)|Merged|| +|`genai` package: Testing|[#882](https://github.com/foss42/apidash/pull/882)||Merged|| +Foundations: Agents & AI UI Designer + Tool Generation |[#874](https://github.com/foss42/apidash/pull/874)||Closed|Mentor requested to make a new PR that was based on top of main branch code| +|AI UI Designer & Tool Generator|[#880](https://github.com/foss42/apidash/pull/880)|[#617](https://github.com/foss42/apidash/issues/617), [#884](https://github.com/foss42/apidash/issues/884)|Merged|| +|Final Report Documentation|[#878](https://github.com/foss42/apidash/pull/878)||Merged|| +--- + +## Challenges Faced + +#### Incomplete Responses after SSE implmentation +After migrating to SSE, API Dash was designed to first listen to a stream and return immediately if the content type wasn’t streaming-related. This worked fine until I discovered an edge case with very long responses: the HTTP protocol splits such responses into multiple packets. Because of the initial stream design, only the first packet was returned, resulting in incomplete outputs. +To fix this, I implemented a manual chunking mechanism where all incoming packets are collected until the stream ends, after which they are concatenated into the complete response. This resolved the issue and ensured correctness for long streaming outputs. + +#### Component Rendering Dilemma +The core feature of the AI UI Designer is an in-app dynamic component renderer. Implementing this is challenging because Dart does not support full runtime reflection for Flutter widgets. In other words, a Flutter program cannot directly execute or render dynamically generated Flutter code at runtime. +I experimented with the available reflection mechanisms in Dart, but they are limited to the language itself and do not extend to Flutter’s widget tree. As a result, I was only able to render very basic elements such as Text widgets. Anything more complex was practically impossible to achieve with Dart’s restricted reflection capabilities. + +Next, I considered using the Dart SDK to build the code into a Flutter Web app and display it to the user through a localhost iframe. However, this would require bundling the Dart SDK with the application, making it significantly heavier. Moreover, it would involve writing platform channel code for macOS, Windows, and other platforms, which would be highly impractical. + +Disappointed with these limitations, I devised a new approach: instead of attempting in-app rendering, I generated the Flutter code and sent it to an external service that could immediately build and deploy it as a Flutter web application, which could then be displayed within an iframe. I implemented this as a project called [FlutRun](https://github.com/synapsecode/AI_UI_designer_prototype) and successfully demonstrated it to the mentors. +As API Dash is a privacy-first API client that prohibits sending user requests to any external servers (with the exception of user specified AI API calls), even routing requests to our own servers is restricted which made this solution impractical to implement. + +Lastly, after some research and discussions with my mentors, I was introduced to the concept of **Server-Driven User Interfaces (SDUI)**. The core idea is to represent UI as a parseable structure (such as JSON) and then dynamically render it using a rendering pipeline written in Flutter. This approach proved to be both practical and efficient. In fact, I came across the Stac package, which implemented this concept seamlessly, and that ultimately became the solution we adopted. + +#### Lack of Error Handling in Stac +Stac (the JSON-based representation of Flutter UIs) is opinionated and differs slightly from native Flutter code. As a result, when LLMs generate Stac code, they often produce small mistakes. + +The challenge is that the Stac framework surfaces these mistakes only as console errors—they don’t bubble up as exceptions to the caller. I attempted to capture them using Flutter’s ErrorZones and similar mechanisms, but without success. + +This limitation is significant because I wanted to implement a reset feature: if the LLM generates invalid Stac code, the app should be able to roll back to the last known good state. With the current design, this isn’t feasible. I even reached out to the Stac founders, who confirmed that proper error bubbling is planned but won’t be available anytime soon. + +The only real workaround right now would be to fork Stac and patch it manually—something I’m still debating. For the time being, we’ve mitigated the issue by tuning the system prompt and restricting generations to a small, well-understood subset of Stac. This approach has been working decently so far. + Have also discussed with the maintainer's of Stac regarding this issue: + + ![Stac Discussion](./images/stacreq.png) + ![Stac Discussion](./images/stacreq2.png) + +#### Stac Code Clipping +When the API response is highly complex, agents may generate a verbose UI specification. This can cause the resulting Stac JSON to become so large that it exceeds the LLM’s output context window, resulting in clipped (incomplete) JSON. +The problem is difficult to detect because Stac lacks proper error handling, so truncated JSON cannot be validated reliably. When this occurs, the system encounters an ugly visual crash. +Unlike plain text, JSON cannot be trivially streamed or concatenated in parts, since partial structures may break schema integrity. At present, this remains an open issue without a clean solution. + +#### Limitations of Prompting +Because of the platform’s privacy-first design, we cannot send user response data to external models. As a result, we must rely entirely on system prompts to enable this feature. +Stac code is highly specialized, and while fine-tuning an existing Flutter-focused or JSON-focused model would have been an effective approach, this option is not permitted under our constraints. This creates a significant challenge, since system prompting alone has limited capacity before context loss begins to degrade output quality. +Our temporary solution is to restrict the feature to a smaller subset of Stac. However, a long-term solution will be necessary to overcome these limitations and support the full scope of functionality. + +--- + +## Future Work +- **Error Handling in Stac** +Stac’s current error handling is limited, making debugging and reliability difficult. A future step would be to improve structured error messages and fallback behaviors for invalid or incomplete code paths. + +- **Expanding Restricted Stac SDUI Widget Library** +At present, the Stac-driven UI generation supports only a subset of widget types. This restricts the richness of UIs generated from complex API responses. Future work can focus on extending the widget library to cover advanced layout controls, interactive inputs, and custom components. This expansion will allow the system to handle more nuanced use cases and generate production-grade UIs directly from structured data. + +- **Integration Tests for AI Features** +Automated testing remains critical to ensure reliability and prevent regressions in AI-driven workflows. Integration tests will be built for tool generation and AI UI Designer + +--- + +## Design and Prototypes Link +- [API Tool Generation Research Document ](https://docs.google.com/document/d/17wjzrJcE-HlSy3i3UdgQUEneCXXEKb-XNNiHSp-ECVg) +- [AI UI Designer prototype](https://github.com/synapsecode/AI_UI_designer_prototype) +- [FlutRun (My custom remote flutter component rendering service)](https://github.com/synapsecode/FlutRun) + +--- + +## Conclusion +Google Summer of Code 2025 with API Dash has been a truly amazing experience. My work throughout this project centered on building the core infrastructure that will be the heart of API Dash's next-gen features, and I believe I have successfully laid a strong foundation. I look forward to seeing future contributors build upon it and take the project even further. + +--- diff --git a/lib/apitoolgen/request_consolidator.dart b/lib/apitoolgen/request_consolidator.dart new file mode 100644 index 00000000..0f86dbe7 --- /dev/null +++ b/lib/apitoolgen/request_consolidator.dart @@ -0,0 +1,97 @@ +class APIDashRequestDescription { + final String endpoint; + final String method; + final Map? queryParams; + final List? formData; + final Map? headers; + final String? bodyTXT; + final Map? bodyJSON; + final String? responseType; + final dynamic response; + + APIDashRequestDescription({ + required this.endpoint, + required this.method, + this.queryParams, + this.formData, + this.headers, + this.bodyTXT, + this.bodyJSON, + this.responseType, + this.response, + }); + + String get generateREQDATA { + //Note Down the Query parameters + String queryParamStr = ''; + if (queryParams != null) { + for (final x in queryParams!.keys) { + queryParamStr += + '\t$x: ${queryParams![x]} <${queryParams![x].runtimeType}>\n'; + } + queryParamStr = 'QUERY_PARAMETERS: {\n$queryParamStr}'; + } + + //Note Down the Headers + String headersStr = ''; + if (headers != null) { + for (final x in headers!.keys) { + headersStr += '\t$x: ${headers![x]} <${headers![x].runtimeType}>\n'; + } + headersStr = 'HEADERS: {\n$headersStr}'; + } + + String bodyDetails = ''; + if (bodyTXT != null) { + bodyDetails = 'BODY_TYPE: TEXT\nBODY_TEXT:$bodyTXT'; + } else if (bodyJSON != null) { + //Note Down the JSONData + String jsonBodyStr = ''; + if (bodyJSON != null) { + getTyp(input, [i = 0]) { + String indent = "\t"; + for (int j = 0; j < i; j++) indent += "\t"; + if (input.runtimeType.toString().toLowerCase().contains('map')) { + String typd = '{'; + for (final z in input.keys) { + typd += "$indent$z: TYPE: ${getTyp(input[z], i + 1)}\n"; + } + return "$indent$typd}"; + } + return input.runtimeType.toString(); + } + + for (final x in bodyJSON!.keys) { + jsonBodyStr += '\t$x: TYPE: <${getTyp(bodyJSON![x])}>\n'; + } + jsonBodyStr = 'BODY_JSON: {\n$jsonBodyStr}'; + } + bodyDetails = 'BODY_TYPE: JSON\n$jsonBodyStr'; + } else if (formData != null) { + //Note Down the FormData + String formDataStr = ''; + if (formData != null) { + for (final x in formData!) { + formDataStr += '\t$x\n'; + } + formDataStr = 'BODY_FORM_DATA: {\n$formDataStr}'; + } + bodyDetails = 'BODY_TYPE: FORM-DATA\n$formDataStr'; + } + + String responseDetails = ''; + if (responseType != null && response != null) { + responseDetails = + '-----RESPONSE_DETAILS-----\nRESPONSE_TYPE: $responseType\nRESPONSE_BODY: $response'; + } + + return """REST API (HTTP) +METHOD: $method +ENDPOINT: $endpoint +HEADERS: ${headersStr.isEmpty ? '{}' : headersStr} +$queryParamStr +$bodyDetails +$responseDetails +"""; + } +} diff --git a/lib/consts.dart b/lib/consts.dart index 71b88ee7..1d7aa6e4 100644 --- a/lib/consts.dart +++ b/lib/consts.dart @@ -510,3 +510,4 @@ const kMsgClearHistory = const kMsgClearHistorySuccess = 'History cleared successfully'; const kMsgClearHistoryError = 'Error clearing history'; const kMsgShareError = "Unable to share"; +const kLabelGenerateUI = "Generate UI"; diff --git a/lib/main.dart b/lib/main.dart index c4457ad6..9be1fb49 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,8 @@ +import 'package:apidash_core/apidash_core.dart'; import 'package:apidash_design_system/apidash_design_system.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stac/stac.dart'; import 'models/models.dart'; import 'providers/providers.dart'; import 'services/services.dart'; @@ -9,6 +11,11 @@ import 'app.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); + await Stac.initialize(); + + //Load all LLMs + // await LLMManager.fetchAvailableLLMs(); + await ModelManager.fetchAvailableModels(); var settingsModel = await getSettingsFromSharedPrefs(); var onboardingStatus = await getOnboardingStatusFromSharedPrefs(); diff --git a/lib/models/settings_model.dart b/lib/models/settings_model.dart index c876279d..b3222bf0 100644 --- a/lib/models/settings_model.dart +++ b/lib/models/settings_model.dart @@ -1,4 +1,5 @@ import 'package:apidash_core/apidash_core.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:apidash/consts.dart'; @@ -213,7 +214,7 @@ class SettingsModel { other.workspaceFolderPath == workspaceFolderPath && other.isSSLDisabled == isSSLDisabled && other.isDashBotEnabled == isDashBotEnabled && - other.defaultAIModel == defaultAIModel; + mapEquals(other.defaultAIModel, defaultAIModel); } @override diff --git a/lib/providers/ai_providers.dart b/lib/providers/ai_providers.dart index 350fea35..9955c010 100644 --- a/lib/providers/ai_providers.dart +++ b/lib/providers/ai_providers.dart @@ -1,5 +1,5 @@ -import 'package:apidash_core/apidash_core.dart'; -import 'package:riverpod/riverpod.dart'; +// import 'package:apidash_core/apidash_core.dart'; +// import 'package:riverpod/riverpod.dart'; -final aiApiCredentialProvider = - StateProvider>((ref) => {}); +// final aiApiCredentialProvider = +// StateProvider>((ref) => {}); diff --git a/lib/screens/common_widgets/agentic_ui_features/ai_ui_designer/framework_selector.dart b/lib/screens/common_widgets/agentic_ui_features/ai_ui_designer/framework_selector.dart new file mode 100644 index 00000000..7cc1eac0 --- /dev/null +++ b/lib/screens/common_widgets/agentic_ui_features/ai_ui_designer/framework_selector.dart @@ -0,0 +1,119 @@ +import 'package:apidash/consts.dart'; +import 'package:apidash_design_system/apidash_design_system.dart'; +import 'package:flutter/material.dart'; + +class FrameWorkSelectorPage extends StatefulWidget { + final String content; + final Function(String, String) onNext; + const FrameWorkSelectorPage( + {super.key, required this.content, required this.onNext}); + + @override + State createState() => _FrameWorkSelectorPageState(); +} + +class _FrameWorkSelectorPageState extends State { + String? selectedFramework; + TextEditingController controller = TextEditingController(); + + @override + void initState() { + controller.text = widget.content; + super.initState(); + } + + @override + Widget build(BuildContext context) { + final textContainerdecoration = BoxDecoration( + color: Color.alphaBlend( + (Theme.of(context).brightness == Brightness.dark + ? Theme.of(context).colorScheme.onPrimaryContainer + : Theme.of(context).colorScheme.primaryContainer) + .withValues(alpha: kForegroundOpacity), + Theme.of(context).colorScheme.surface), + border: Border.all( + color: Theme.of(context).colorScheme.surfaceContainerHighest), + borderRadius: kBorderRadius8, + ); + + return Container( + // width: MediaQuery.of(context).size.width * 0.6, // Large dialog + padding: EdgeInsets.all(20), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Container( + width: double.maxFinite, + padding: kP8, + decoration: textContainerdecoration, + child: SingleChildScrollView( + child: TextField( + controller: controller, + maxLines: null, + style: kCodeStyle, + ), + ), + ), + ), + kVSpacer20, + // Text( + // "Select Framework", + // style: TextStyle( + // color: Colors.white, + // fontSize: 18, + // fontWeight: FontWeight.bold, + // ), + // ), + // SizedBox(height: 10), + // DropdownButtonFormField( + // dropdownColor: Color(0xFF2D2D2D), + // decoration: InputDecoration( + // filled: true, + // fillColor: Color(0xFF2D2D2D), + // border: OutlineInputBorder( + // borderRadius: BorderRadius.circular(8.0), + // ), + // ), + // value: selectedFramework, + // items: ["Flutter", "ReactJS"].map((String value) { + // return DropdownMenuItem( + // value: value, + // child: Text( + // value, + // style: TextStyle(color: Colors.white), + // ), + // ); + // }).toList(), + // onChanged: (newValue) { + // selectedFramework = newValue; + // setState(() {}); + // }, + // ), + // kVSpacer20, + Align( + alignment: Alignment.centerRight, + child: FilledButton.tonalIcon( + style: FilledButton.styleFrom( + padding: kPh12, + minimumSize: const Size(44, 44), + ), + onPressed: () { + widget.onNext(controller.value.text, "FLUTTER"); + }, + icon: Icon( + Icons.generating_tokens, + ), + label: const SizedBox( + child: Text( + kLabelGenerateUI, + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/screens/common_widgets/agentic_ui_features/ai_ui_designer/generate_ui_dialog.dart b/lib/screens/common_widgets/agentic_ui_features/ai_ui_designer/generate_ui_dialog.dart new file mode 100644 index 00000000..33967d8f --- /dev/null +++ b/lib/screens/common_widgets/agentic_ui_features/ai_ui_designer/generate_ui_dialog.dart @@ -0,0 +1,191 @@ +import 'package:apidash/consts.dart'; +import 'package:apidash/providers/collection_providers.dart'; +import 'package:apidash/services/agentic_services/apidash_agent_calls.dart'; +import 'package:apidash/widgets/widget_sending.dart'; +import 'package:apidash_design_system/apidash_design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'framework_selector.dart'; +import 'sdui_preview.dart'; + +void showCustomDialog(BuildContext context, Widget dialogContent) { + showDialog( + context: context, + builder: (BuildContext context) { + return Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.0), + ), + child: dialogContent, + ); + }, + ); +} + +class GenerateUIDialog extends ConsumerStatefulWidget { + final String content; + const GenerateUIDialog({ + super.key, + required this.content, + }); + + @override + ConsumerState createState() => _GenerateUIDialogState(); +} + +class _GenerateUIDialogState extends ConsumerState { + int index = 0; + TextEditingController controller = TextEditingController(); + + String generatedSDUI = '{}'; + + Future generateSDUICode(String apiResponse) async { + try { + setState(() { + index = 1; //Induce Loading + }); + final res = await generateSDUICodeFromResponse( + ref: ref, + apiResponse: apiResponse, + ); + if (res == null) { + setState(() { + index = 0; + }); + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text( + "Preview Generation Failed!", + style: TextStyle(color: Colors.white), + ), + backgroundColor: Colors.redAccent, + )); + return null; + } + return res; + } catch (e) { + String errMsg = 'Unexpected Error Occured'; + if (e.toString().contains('NO_DEFAULT_LLM')) { + errMsg = "Please Select Default AI Model in Settings"; + } + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text( + errMsg, + style: TextStyle(color: Colors.white), + ), + backgroundColor: Colors.redAccent, + )); + Navigator.pop(context); + return null; + } + } + + Future modifySDUICode(String modificationRequest) async { + setState(() { + index = 1; //Induce Loading + }); + final res = await modifySDUICodeUsingPrompt( + generatedSDUI: generatedSDUI, + ref: ref, + modificationRequest: modificationRequest, + ); + if (res == null) { + setState(() { + index = 2; + }); + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text( + "Modification Request Failed!", + style: TextStyle(color: Colors.white), + ), + backgroundColor: Colors.redAccent, + )); + return; + } + setState(() { + generatedSDUI = res; + index = 2; + }); + } + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + if (index == 0) + FrameWorkSelectorPage( + content: widget.content, + onNext: (apiResponse, targetLanguage) async { + print("Generating SDUI Code"); + final sdui = await generateSDUICode(apiResponse); + if (sdui == null) return; + setState(() { + index = 2; + generatedSDUI = sdui; + }); + }, + ), + if (index == 1) + SizedBox( + // width: MediaQuery.of(context).size.width * 0.6, + child: Center( + child: Padding( + padding: const EdgeInsets.only(top: 40.0), + child: Container( + height: 500, + child: SendingWidget( + startSendingTime: DateTime.now(), + showTimeElapsed: false, + ), + ), + ), + ), + ), + if (index == 2) + SDUIPreviewPage( + key: ValueKey(generatedSDUI.hashCode), + onModificationRequestMade: modifySDUICode, + sduiCode: generatedSDUI, + ) + ], + ); + } +} + +class AIGenerateUIButton extends ConsumerWidget { + const AIGenerateUIButton({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return FilledButton.tonalIcon( + style: FilledButton.styleFrom( + padding: kPh12, + minimumSize: const Size(44, 44), + ), + onPressed: () { + final model = ref.watch(selectedRequestModelProvider + .select((value) => value?.httpResponseModel)); + if (model == null) return; + + String data = ""; + if (model.sseOutput != null) { + data = model.sseOutput!.join(''); + } else { + data = model.formattedBody ?? "<>"; + } + + showCustomDialog( + context, + GenerateUIDialog(content: data), + ); + }, + icon: Icon( + Icons.generating_tokens, + ), + label: const SizedBox( + child: Text( + kLabelGenerateUI, + ), + ), + ); + } +} diff --git a/lib/screens/common_widgets/agentic_ui_features/ai_ui_designer/sdui_preview.dart b/lib/screens/common_widgets/agentic_ui_features/ai_ui_designer/sdui_preview.dart new file mode 100644 index 00000000..a984533d --- /dev/null +++ b/lib/screens/common_widgets/agentic_ui_features/ai_ui_designer/sdui_preview.dart @@ -0,0 +1,195 @@ +import 'package:apidash/screens/common_widgets/agentic_ui_features/ai_ui_designer/sdui_renderer.dart'; +import 'package:apidash/services/agentic_services/agent_caller.dart'; +import 'package:apidash/services/agentic_services/agents/stac_to_flutter.dart'; +import 'package:apidash_core/apidash_core.dart'; +import 'package:apidash_design_system/apidash_design_system.dart'; +import 'package:apidash_design_system/tokens/measurements.dart'; +import 'package:apidash_design_system/widgets/textfield_outlined.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class SDUIPreviewPage extends ConsumerStatefulWidget { + final String sduiCode; + final Function(String) onModificationRequestMade; + const SDUIPreviewPage({ + super.key, + required this.onModificationRequestMade, + required this.sduiCode, + }); + + @override + ConsumerState createState() => _SDUIPreviewPageState(); +} + +class _SDUIPreviewPageState extends ConsumerState { + bool exportingCode = false; + String modificationRequest = ""; + + exportCode() async { + setState(() { + exportingCode = true; + }); + final ans = await APIDashAgentCaller.instance.call( + StacToFlutterBot(), + ref: ref, + input: AgentInputs( + variables: {'VAR_CODE': widget.sduiCode}, + ), + ); + final exportedCode = ans?['CODE']; + + if (exportedCode == null) { + setState(() { + exportingCode = false; + }); + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text( + "Export Failed", + style: TextStyle(color: Colors.white), + ), + backgroundColor: Colors.redAccent, + )); + print("exportCode: Failed; ABORTING"); + return; + } + + Clipboard.setData(ClipboardData(text: ans['CODE'])); + ScaffoldMessenger.of(context) + .showSnackBar(SnackBar(content: Text("Copied to clipboard!"))); + setState(() { + exportingCode = false; + }); + } + + @override + Widget build(BuildContext context) { + return Container( + // width: MediaQuery.of(context).size.width * 0.6, // Large dialog + padding: EdgeInsets.all(20), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + "Generated Component", + style: TextStyle( + fontSize: 20, + ), + ), + Spacer(), + IconButton( + onPressed: () { + Navigator.pop(context); + }, + icon: Icon(Icons.close)), + ], + ), + kVSpacer20, + Expanded( + child: Center( + child: Container( + padding: EdgeInsets.all(20), + decoration: BoxDecoration( + border: Border.all(color: Colors.white), + ), + child: AspectRatio( + aspectRatio: 16 / 9, + child: StacRenderer( + stacRepresentation: widget.sduiCode, + onError: () { + Future.delayed(Duration(milliseconds: 200), () { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text( + "Failed to Display Preview", + style: TextStyle(color: Colors.white), + ), + backgroundColor: Colors.redAccent, + )); + }); + }, + ), + ), + ), + ), + ), + kVSpacer20, + if (!exportingCode) ...[ + Container( + width: double.infinity, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + ), + child: ADOutlinedTextField( + hintText: 'Any Modifications?', + onChanged: (z) { + setState(() { + modificationRequest = z; + }); + }, + maxLines: 3, // Makes the text box taller + ), + ), + kVSpacer20, + ], + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Align( + alignment: Alignment.centerRight, + child: (exportingCode) + ? Container( + child: CircularProgressIndicator( + strokeWidth: 1, + ), + margin: EdgeInsets.only(right: 10), + ) + : FilledButton.tonalIcon( + style: FilledButton.styleFrom( + padding: kPh12, + minimumSize: const Size(44, 44), + ), + onPressed: exportCode, + icon: Icon( + Icons.download, + ), + label: const SizedBox( + child: Text( + "Export Code", + ), + ), + ), + ), + kHSpacer10, + if (!exportingCode) + Align( + alignment: Alignment.centerRight, + child: FilledButton.tonalIcon( + style: FilledButton.styleFrom( + padding: kPh12, + minimumSize: const Size(44, 44), + ), + onPressed: () { + if (modificationRequest.isNotEmpty) { + widget.onModificationRequestMade(modificationRequest); + } + }, + icon: Icon( + Icons.generating_tokens, + ), + label: const SizedBox( + child: Text( + "Make Modifications", + ), + ), + ), + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/screens/common_widgets/agentic_ui_features/ai_ui_designer/sdui_renderer.dart b/lib/screens/common_widgets/agentic_ui_features/ai_ui_designer/sdui_renderer.dart new file mode 100644 index 00000000..a9128475 --- /dev/null +++ b/lib/screens/common_widgets/agentic_ui_features/ai_ui_designer/sdui_renderer.dart @@ -0,0 +1,45 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:stac/stac.dart' as stac; + +class StacRenderer extends StatefulWidget { + final String stacRepresentation; + final VoidCallback onError; + const StacRenderer( + {super.key, required this.stacRepresentation, required this.onError}); + + @override + State createState() => _StacRendererState(); +} + +class _StacRendererState extends State { + Map? sduiCode; + + @override + void initState() { + super.initState(); + try { + sduiCode = jsonDecode(widget.stacRepresentation); + } catch (e) { + widget.onError(); + } + } + + @override + Widget build(BuildContext context) { + // return SingleChildScrollView( + // child: SelectableText(sduiCode?.toString() ?? ""), + // ); + if (sduiCode == null || sduiCode!.isEmpty) { + return Container(); + } + return stac.StacApp( + title: 'Component Preview', + homeBuilder: (context) => Material( + color: Colors.transparent, + child: stac.Stac.fromJson(sduiCode!.cast(), context), + ), + ); + } +} diff --git a/lib/screens/common_widgets/agentic_ui_features/tool_generation/generate_tool_dialog.dart b/lib/screens/common_widgets/agentic_ui_features/tool_generation/generate_tool_dialog.dart new file mode 100644 index 00000000..f7f7b510 --- /dev/null +++ b/lib/screens/common_widgets/agentic_ui_features/tool_generation/generate_tool_dialog.dart @@ -0,0 +1,207 @@ +import 'package:apidash/apitoolgen/request_consolidator.dart'; +import 'package:apidash/providers/collection_providers.dart'; +import 'package:apidash/screens/common_widgets/agentic_ui_features/ai_ui_designer/generate_ui_dialog.dart'; +import 'package:apidash/services/agentic_services/apidash_agent_calls.dart'; +import 'package:apidash_core/apidash_core.dart'; +import 'package:apidash_design_system/apidash_design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'generated_tool_codecopy.dart'; +import 'tool_requirements_selector.dart'; + +class GenerateToolButton extends ConsumerWidget { + const GenerateToolButton({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return FilledButton.tonalIcon( + style: FilledButton.styleFrom( + padding: kPh12, + minimumSize: const Size(44, 44), + ), + onPressed: () async { + GenerateToolDialog.show(context, ref); + }, + icon: Icon( + Icons.token_outlined, + ), + label: const SizedBox( + child: Text( + "Generate Tool", + ), + ), + ); + } +} + +class GenerateToolDialog extends ConsumerStatefulWidget { + final APIDashRequestDescription requestDesc; + const GenerateToolDialog({ + super.key, + required this.requestDesc, + }); + + static show(BuildContext context, WidgetRef ref) { + final aiRequestModel = ref.watch( + selectedRequestModelProvider.select((value) => value?.aiRequestModel)); + HttpRequestModel? requestModel = ref.watch(selectedRequestModelProvider + .select((value) => value?.httpRequestModel)); + final responseModel = ref.watch(selectedRequestModelProvider + .select((value) => value?.httpResponseModel)); + + if (aiRequestModel == null && requestModel == null) return; + if (requestModel == null) return; + if (responseModel == null) return; + + String? bodyTXT; + Map? bodyJSON; + List? bodyFormData; + + if (aiRequestModel != null) { + requestModel = aiRequestModel.httpRequestModel!; + } + + final reqDesModel = APIDashRequestDescription( + endpoint: requestModel.url, + method: requestModel.method.name.toUpperCase(), + responseType: responseModel.contentType.toString(), + headers: requestModel.headersMap, + response: responseModel.body, + formData: bodyFormData, + bodyTXT: bodyTXT, + bodyJSON: bodyJSON, + ); + + showCustomDialog( + context, + GenerateToolDialog( + requestDesc: reqDesModel, + ), + ); + } + + @override + ConsumerState createState() => _GenerateToolDialogState(); +} + +class _GenerateToolDialogState extends ConsumerState { + int index = 0; + TextEditingController controller = TextEditingController(); + + String selectedLanguage = 'PYTHON'; + String selectedAgent = 'GEMINI'; + String? generatedToolCode = ''; + + generateAPITool() async { + try { + setState(() { + generatedToolCode = null; + index = 1; + }); + final res = await generateAPIToolUsingRequestData( + ref: ref, + requestData: widget.requestDesc.generateREQDATA, + targetLanguage: selectedLanguage, + selectedAgent: selectedAgent, + ); + if (res == null) { + setState(() { + generatedToolCode = ''; + index = 0; + }); + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text( + "API Tool generation failed!", + style: TextStyle(color: Colors.white), + ), + backgroundColor: Colors.redAccent, + )); + return; + } + setState(() { + generatedToolCode = res; + index = 1; + }); + } catch (e) { + setState(() { + index = 0; + }); + String errMsg = 'Unexpected Error Occured'; + if (e.toString().contains('NO_DEFAULT_LLM')) { + errMsg = "Please Select Default AI Model in Settings"; + } + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text( + errMsg, + style: TextStyle(color: Colors.white), + ), + backgroundColor: Colors.redAccent, + )); + Navigator.pop(context); + } + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder(builder: (context, constraints) { + final dialogWidth = constraints.maxWidth; + final isExpandedWindow = dialogWidth > WindowWidth.expanded.value; + final isLargeWindow = dialogWidth > WindowWidth.large.value; + final isExtraLargeWindow = dialogWidth > WindowWidth.large.value; + + if (isExtraLargeWindow || isLargeWindow || isExpandedWindow) { + return Container( + height: 600, + width: MediaQuery.of(context).size.width * 0.8, + child: Row( + children: [ + Flexible( + flex: 2, + child: ToolRequirementSelectorPage( + onGenerateCallback: (agent, lang) { + setState(() { + selectedLanguage = lang; + selectedAgent = agent; + }); + generateAPITool(); + }, + )), + Expanded( + flex: 3, + child: GeneratedToolCodeCopyPage( + toolCode: generatedToolCode, + language: selectedLanguage.trim(), + ), + ), + ], + ), + ); + } else { + return Container( + height: 600, + // width: MediaQuery.of(context).size.width * 0.8, + child: IndexedStack( + index: index, + children: [ + Center( + child: ToolRequirementSelectorPage( + onGenerateCallback: (agent, lang) { + setState(() { + selectedLanguage = lang; + selectedAgent = agent; + }); + generateAPITool(); + }, + ), + ), + GeneratedToolCodeCopyPage( + toolCode: generatedToolCode, + language: selectedLanguage.trim(), + ), + ], + ), + ); + } + }); + } +} diff --git a/lib/screens/common_widgets/agentic_ui_features/tool_generation/generated_tool_codecopy.dart b/lib/screens/common_widgets/agentic_ui_features/tool_generation/generated_tool_codecopy.dart new file mode 100644 index 00000000..bf3bf34a --- /dev/null +++ b/lib/screens/common_widgets/agentic_ui_features/tool_generation/generated_tool_codecopy.dart @@ -0,0 +1,65 @@ +import 'package:apidash/widgets/button_copy.dart'; +import 'package:apidash/widgets/previewer_code.dart'; +import 'package:apidash/widgets/widget_sending.dart'; +import 'package:apidash_design_system/tokens/tokens.dart'; +import 'package:flutter/material.dart'; + +class GeneratedToolCodeCopyPage extends StatelessWidget { + final String? toolCode; + final String language; + const GeneratedToolCodeCopyPage( + {super.key, required this.toolCode, required this.language}); + + @override + Widget build(BuildContext context) { + final lightMode = Theme.of(context).brightness == Brightness.light; + var codeTheme = lightMode ? kLightCodeTheme : kDarkCodeTheme; + + if (toolCode == null) { + return SendingWidget( + startSendingTime: DateTime.now(), + showTimeElapsed: false, + ); + } + + if (toolCode!.isEmpty) { + return Padding( + padding: const EdgeInsets.only(right: 40), + child: Center( + child: Icon( + Icons.token_outlined, + color: lightMode ? Colors.black12 : Colors.white12, + size: 500, + ), + ), + ); + } + + return Container( + color: const Color.fromARGB(26, 123, 123, 123), + padding: EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + CopyButton( + toCopy: toolCode!, + showLabel: true, + ), + Expanded( + child: SingleChildScrollView( + child: Container( + width: double.infinity, + child: CodePreviewer( + code: toolCode!, + theme: codeTheme, + language: language.toLowerCase(), + textStyle: kCodeStyle, + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/screens/common_widgets/agentic_ui_features/tool_generation/tool_requirements_selector.dart b/lib/screens/common_widgets/agentic_ui_features/tool_generation/tool_requirements_selector.dart new file mode 100644 index 00000000..68556b06 --- /dev/null +++ b/lib/screens/common_widgets/agentic_ui_features/tool_generation/tool_requirements_selector.dart @@ -0,0 +1,203 @@ +import 'package:apidash/providers/settings_providers.dart'; +import 'package:apidash/screens/common_widgets/ai/ai_model_selector_button.dart'; +import 'package:apidash_core/apidash_core.dart'; +import 'package:apidash_design_system/apidash_design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class ToolRequirementSelectorPage extends StatefulWidget { + final Function(String agent, String lang) onGenerateCallback; + const ToolRequirementSelectorPage( + {super.key, required this.onGenerateCallback}); + + @override + State createState() => + _ToolRequirementSelectorPageState(); +} + +class _ToolRequirementSelectorPageState + extends State { + String targetLanguage = 'PYTHON'; + String agentFramework = 'GEMINI'; + + Map frameworkMapping = { + 'GEMINI': 'Gemini', + 'OPENAI': 'OpenAI', + 'LANGCHAIN': 'LangChain', + 'MICROSOFT_AUTOGEN': 'Microsoft AutoGen', + 'MISTRAL': 'Mistral', + 'ANTRHOPIC': 'Anthropic', + }; + + Map languageMapping = { + 'PYTHON': 'Python 3', + 'JAVASCRIPT': 'JavaScript / NodeJS' + }; + + @override + Widget build(BuildContext context) { + final lightMode = Theme.of(context).brightness == Brightness.light; + + return Container( + constraints: BoxConstraints.expand(), + padding: EdgeInsets.all(30), + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + "Generate API Tool", + style: TextStyle( + fontSize: 24, + ), + ), + kVSpacer5, + Padding( + padding: EdgeInsets.only(left: 3), + child: Text( + "Select an agent framework & language", + style: TextStyle( + color: lightMode ? Colors.black54 : Colors.white60, + fontSize: 15), + ), + ), + kVSpacer20, + Padding( + padding: EdgeInsets.only(left: 3), + child: Text( + "Agent Framework", + style: TextStyle( + color: lightMode ? Colors.black54 : Colors.white60, + ), + ), + ), + kVSpacer8, + ADPopupMenu( + value: frameworkMapping[agentFramework], + values: [ + ...frameworkMapping.keys + .map((e) => (e.toString(), frameworkMapping[e].toString())), + ], + width: MediaQuery.of(context).size.width * 0.35, + tooltip: '', + onChanged: (x) { + setState(() { + agentFramework = x ?? 'OPENAI'; + + //AutoGen is Python-Only + if (agentFramework == 'MICROSOFT_AUTOGEN') { + targetLanguage = 'PYTHON'; + } + }); + }, + isOutlined: true, + ), + kVSpacer20, + Padding( + padding: EdgeInsets.only(left: 3), + child: Text( + "Target Language", + style: TextStyle( + color: lightMode ? Colors.black54 : Colors.white60, + ), + ), + ), + kVSpacer8, + ADPopupMenu( + value: languageMapping[targetLanguage], + values: [ + ...languageMapping.keys + .map((e) => (e.toString(), languageMapping[e].toString())), + ], + width: MediaQuery.of(context).size.width * 0.35, + tooltip: '', + onChanged: (x) { + setState(() { + targetLanguage = x ?? 'PYTHON'; + + //AutoGen is Python-Only + if (agentFramework == 'MICROSOFT_AUTOGEN') { + targetLanguage = 'PYTHON'; + } + }); + }, + isOutlined: true, + ), + kVSpacer20, + Wrap( + runSpacing: 10, + alignment: WrapAlignment.center, + runAlignment: WrapAlignment.center, + // mainAxisAlignment: MainAxisAlignment.center, + children: [ + FilledButton.tonalIcon( + style: FilledButton.styleFrom( + padding: kPh12, + minimumSize: const Size(44, 44), + ), + onPressed: () { + widget.onGenerateCallback(agentFramework, targetLanguage); + }, + icon: Icon( + Icons.token_outlined, + ), + label: const SizedBox( + child: Text( + "Generate Tool", + ), + ), + ), + kHSpacer5, + DefaultLLModelSelectorWidget(), + ], + ), + ], + ), + ); + } +} + +class DefaultLLModelSelectorWidget extends ConsumerWidget { + const DefaultLLModelSelectorWidget({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final settings = ref.watch(settingsProvider); + return Opacity( + opacity: 0.8, + child: Container( + width: 200, + child: Row( + children: [ + Padding( + padding: EdgeInsets.only(left: 3), + child: Text( + "with", + style: TextStyle( + color: Theme.of(context).brightness == Brightness.light + ? Colors.black54 + : Colors.white60, + fontSize: 15), + ), + ), + SizedBox(width: 5), + AIModelSelectorButton( + aiRequestModel: + AIRequestModel.fromJson(settings.defaultAIModel ?? {}), + onModelUpdated: (d) { + ref.read(settingsProvider.notifier).update( + defaultAIModel: d.copyWith( + modelConfigs: [], + stream: null, + systemPrompt: '', + userPrompt: '').toJson()); + }, + ), + kVSpacer5, + ], + ), + ), + ); + } +} diff --git a/lib/screens/common_widgets/ai/ai_model_selector_dialog.dart b/lib/screens/common_widgets/ai/ai_model_selector_dialog.dart index be6f7e73..53680e22 100644 --- a/lib/screens/common_widgets/ai/ai_model_selector_dialog.dart +++ b/lib/screens/common_widgets/ai/ai_model_selector_dialog.dart @@ -1,4 +1,4 @@ -import 'package:apidash/providers/providers.dart'; +// import 'package:apidash/providers/providers.dart'; import 'package:apidash/widgets/widgets.dart'; import 'package:apidash_core/apidash_core.dart'; import 'package:apidash_design_system/apidash_design_system.dart'; @@ -31,7 +31,7 @@ class _AIModelSelectorDialogState extends ConsumerState { @override Widget build(BuildContext context) { - ref.watch(aiApiCredentialProvider); + // ref.watch(aiApiCredentialProvider); final width = MediaQuery.of(context).size.width * 0.8; return FutureBuilder( future: aM, @@ -147,8 +147,8 @@ class _AIModelSelectorDialogState extends ConsumerState { if (aiModelProvider == null) { return Center(child: Text("Please select an AI API Provider")); } - final currentCredential = - ref.watch(aiApiCredentialProvider)[aiModelProvider.providerId!] ?? ""; + // final currentCredential = + // ref.watch(aiApiCredentialProvider)[aiModelProvider.providerId!] ?? ""; return Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.max, @@ -163,12 +163,16 @@ class _AIModelSelectorDialogState extends ConsumerState { kVSpacer8, BoundedTextField( onChanged: (x) { - ref.read(aiApiCredentialProvider.notifier).state = { - ...ref.read(aiApiCredentialProvider), - aiModelProvider.providerId!: x - }; + // ref.read(aiApiCredentialProvider.notifier).state = { + // ...ref.read(aiApiCredentialProvider), + // aiModelProvider.providerId!: x + // }; + setState(() { + newAIRequestModel = newAIRequestModel?.copyWith(apiKey: x); + }); }, - value: currentCredential, + value: newAIRequestModel?.apiKey ?? "", + // value: currentCredential, ), kVSpacer10, ], diff --git a/lib/screens/history/history_widgets/his_response_pane.dart b/lib/screens/history/history_widgets/his_response_pane.dart index 695c3785..b63b2d2a 100644 --- a/lib/screens/history/history_widgets/his_response_pane.dart +++ b/lib/screens/history/history_widgets/his_response_pane.dart @@ -35,6 +35,7 @@ class HistoryResponsePane extends ConsumerWidget { children: [ ResponseBody( selectedRequestModel: requestModel, + isPartOfHistory: true, ), ResponseHeaders( responseHeaders: historyHttpResponseModel?.headers ?? {}, diff --git a/lib/services/agentic_services/agent_caller.dart b/lib/services/agentic_services/agent_caller.dart new file mode 100644 index 00000000..c9939770 --- /dev/null +++ b/lib/services/agentic_services/agent_caller.dart @@ -0,0 +1,27 @@ +import 'package:apidash/providers/providers.dart'; +import 'package:apidash_core/apidash_core.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class APIDashAgentCaller { + static APIDashAgentCaller instance = APIDashAgentCaller(); + + Future call( + AIAgent agent, { + required WidgetRef ref, + required AgentInputs input, + }) async { + final defaultAIModel = + ref.read(settingsProvider.select((e) => e.defaultAIModel)); + if (defaultAIModel == null) { + throw Exception('NO_DEFAULT_LLM'); + } + final baseAIRequestObject = AIRequestModel.fromJson(defaultAIModel); + final ans = await AIAgentService.callAgent( + agent, + baseAIRequestObject, + query: input.query, + variables: input.variables, + ); + return ans; + } +} diff --git a/lib/services/agentic_services/agents/agents.dart b/lib/services/agentic_services/agents/agents.dart new file mode 100644 index 00000000..e0b355ba --- /dev/null +++ b/lib/services/agentic_services/agents/agents.dart @@ -0,0 +1,7 @@ +export 'intermediate_rep_gen.dart'; +export 'semantic_analyser.dart'; +export 'stac_to_flutter.dart'; +export 'stacgen.dart'; +export 'stacmodifier.dart'; +export 'apitool_funcgen.dart'; +export 'apitool_bodygen.dart'; diff --git a/lib/services/agentic_services/agents/apitool_bodygen.dart b/lib/services/agentic_services/agents/apitool_bodygen.dart new file mode 100644 index 00000000..628d777f --- /dev/null +++ b/lib/services/agentic_services/agents/apitool_bodygen.dart @@ -0,0 +1,32 @@ +import 'package:apidash/templates/templates.dart'; +import 'package:apidash_core/apidash_core.dart'; + +class ApiToolBodyGen extends AIAgent { + @override + String get agentName => 'APITOOL_BODYGEN'; + + @override + String getSystemPrompt() { + return kPromptAPIToolBodyGen; + } + + @override + Future validator(String aiResponse) async { + //Add any specific validations here as needed + return true; + } + + @override + Future outputFormatter(String validatedResponse) async { + validatedResponse = validatedResponse + .replaceAll('```python', '') + .replaceAll('```python\n', '') + .replaceAll('```javascript', '') + .replaceAll('```javascript\n', '') + .replaceAll('```', ''); + + return { + 'TOOL': validatedResponse, + }; + } +} diff --git a/lib/services/agentic_services/agents/apitool_funcgen.dart b/lib/services/agentic_services/agents/apitool_funcgen.dart new file mode 100644 index 00000000..b870332f --- /dev/null +++ b/lib/services/agentic_services/agents/apitool_funcgen.dart @@ -0,0 +1,32 @@ +import 'package:apidash/templates/templates.dart'; +import 'package:apidash_core/apidash_core.dart'; + +class APIToolFunctionGenerator extends AIAgent { + @override + String get agentName => 'APITOOL_FUNCGEN'; + + @override + String getSystemPrompt() { + return kPromptAPIToolFuncGen; + } + + @override + Future validator(String aiResponse) async { + //Add any specific validations here as needed + return true; + } + + @override + Future outputFormatter(String validatedResponse) async { + validatedResponse = validatedResponse + .replaceAll('```python', '') + .replaceAll('```python\n', '') + .replaceAll('```javascript', '') + .replaceAll('```javascript\n', '') + .replaceAll('```', ''); + + return { + 'FUNC': validatedResponse, + }; + } +} diff --git a/lib/services/agentic_services/agents/intermediate_rep_gen.dart b/lib/services/agentic_services/agents/intermediate_rep_gen.dart new file mode 100644 index 00000000..f8523555 --- /dev/null +++ b/lib/services/agentic_services/agents/intermediate_rep_gen.dart @@ -0,0 +1,29 @@ +import 'package:apidash/templates/templates.dart'; +import 'package:apidash_core/apidash_core.dart'; + +class IntermediateRepresentationGen extends AIAgent { + @override + String get agentName => 'INTERMEDIATE_REP_GEN'; + + @override + String getSystemPrompt() { + return kPromptIntermediateRepGen; + } + + @override + Future validator(String aiResponse) async { + //Add any specific validations here as needed + return true; + } + + @override + Future outputFormatter(String validatedResponse) async { + validatedResponse = validatedResponse + .replaceAll('```yaml', '') + .replaceAll('```yaml\n', '') + .replaceAll('```', ''); + return { + 'INTERMEDIATE_REPRESENTATION': validatedResponse, + }; + } +} diff --git a/lib/services/agentic_services/agents/semantic_analyser.dart b/lib/services/agentic_services/agents/semantic_analyser.dart new file mode 100644 index 00000000..861a5cac --- /dev/null +++ b/lib/services/agentic_services/agents/semantic_analyser.dart @@ -0,0 +1,25 @@ +import 'package:apidash/templates/templates.dart'; +import 'package:apidash_core/apidash_core.dart'; + +class ResponseSemanticAnalyser extends AIAgent { + @override + String get agentName => 'RESP_SEMANTIC_ANALYSER'; + + @override + String getSystemPrompt() { + return kPromptSemanticAnalyser; + } + + @override + Future validator(String aiResponse) async { + //Add any specific validations here as needed + return true; + } + + @override + Future outputFormatter(String validatedResponse) async { + return { + 'SEMANTIC_ANALYSIS': validatedResponse, + }; + } +} diff --git a/lib/services/agentic_services/agents/stac_to_flutter.dart b/lib/services/agentic_services/agents/stac_to_flutter.dart new file mode 100644 index 00000000..af0b15c8 --- /dev/null +++ b/lib/services/agentic_services/agents/stac_to_flutter.dart @@ -0,0 +1,30 @@ +import 'package:apidash/templates/templates.dart'; +import 'package:apidash_core/apidash_core.dart'; + +class StacToFlutterBot extends AIAgent { + @override + String get agentName => 'STAC_TO_FLUTTER'; + + @override + String getSystemPrompt() { + return kPromptStacToFlutter; + } + + @override + Future validator(String aiResponse) async { + //Add any specific validations here as needed + return true; + } + + @override + Future outputFormatter(String validatedResponse) async { + validatedResponse = validatedResponse + .replaceAll('```dart', '') + .replaceAll('```dart\n', '') + .replaceAll('```', ''); + + return { + 'CODE': validatedResponse, + }; + } +} diff --git a/lib/services/agentic_services/agents/stacgen.dart b/lib/services/agentic_services/agents/stacgen.dart new file mode 100644 index 00000000..2c1cac19 --- /dev/null +++ b/lib/services/agentic_services/agents/stacgen.dart @@ -0,0 +1,41 @@ +import 'dart:convert'; +import 'package:apidash/templates/templates.dart'; +import 'package:apidash_core/apidash_core.dart'; + +class StacGenBot extends AIAgent { + @override + String get agentName => 'STAC_GEN'; + + @override + String getSystemPrompt() { + return kPromptStacGen; + } + + @override + Future validator(String aiResponse) async { + aiResponse = aiResponse.replaceAll('```json', '').replaceAll('```', ''); + //JSON CHECK + try { + jsonDecode(aiResponse); + } catch (e) { + print("JSON PARSE ERROR: ${e}"); + return false; + } + return true; + } + + @override + Future outputFormatter(String validatedResponse) async { + validatedResponse = validatedResponse + .replaceAll('```json', '') + .replaceAll('```json\n', '') + .replaceAll('```', ''); + + //Stac Specific Changes + validatedResponse = validatedResponse.replaceAll('bold', 'w700'); + + return { + 'STAC': validatedResponse, + }; + } +} diff --git a/lib/services/agentic_services/agents/stacmodifier.dart b/lib/services/agentic_services/agents/stacmodifier.dart new file mode 100644 index 00000000..acc10524 --- /dev/null +++ b/lib/services/agentic_services/agents/stacmodifier.dart @@ -0,0 +1,41 @@ +import 'dart:convert'; +import 'package:apidash/templates/templates.dart'; +import 'package:apidash_core/apidash_core.dart'; + +class StacModifierBot extends AIAgent { + @override + String get agentName => 'STAC_MODIFIER'; + + @override + String getSystemPrompt() { + return kPromptStacModifier; + } + + @override + Future validator(String aiResponse) async { + aiResponse = aiResponse.replaceAll('```json', '').replaceAll('```', ''); + //JSON CHECK + try { + jsonDecode(aiResponse); + } catch (e) { + print("JSON PARSE ERROR: ${e}"); + return false; + } + return true; + } + + @override + Future outputFormatter(String validatedResponse) async { + validatedResponse = validatedResponse + .replaceAll('```json', '') + .replaceAll('```json\n', '') + .replaceAll('```', ''); + + //Stac Specific Changes + validatedResponse = validatedResponse.replaceAll('bold', 'w700'); + + return { + 'STAC': validatedResponse, + }; + } +} diff --git a/lib/services/agentic_services/apidash_agent_calls.dart b/lib/services/agentic_services/apidash_agent_calls.dart new file mode 100644 index 00000000..03dd4f62 --- /dev/null +++ b/lib/services/agentic_services/apidash_agent_calls.dart @@ -0,0 +1,103 @@ +import 'package:apidash/services/agentic_services/agent_caller.dart'; +import 'package:apidash/services/agentic_services/agents/agents.dart'; +import 'package:apidash/templates/tool_templates.dart'; +import 'package:apidash_core/apidash_core.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +Future generateSDUICodeFromResponse({ + required WidgetRef ref, + required String apiResponse, +}) async { + final step1Res = await Future.wait([ + APIDashAgentCaller.instance.call( + ResponseSemanticAnalyser(), + ref: ref, + input: AgentInputs(query: apiResponse), + ), + APIDashAgentCaller.instance.call( + IntermediateRepresentationGen(), + ref: ref, + input: AgentInputs(variables: { + 'VAR_API_RESPONSE': apiResponse, + }), + ), + ]); + final SA = step1Res[0]?['SEMANTIC_ANALYSIS']; + final IR = step1Res[1]?['INTERMEDIATE_REPRESENTATION']; + + if (SA == null || IR == null) { + return null; + } + + print("Semantic Analysis: $SA"); + print("Intermediate Representation: $IR"); + + final sduiCode = await APIDashAgentCaller.instance.call( + StacGenBot(), + ref: ref, + input: AgentInputs(variables: { + 'VAR_RAW_API_RESPONSE': apiResponse, + 'VAR_INTERMEDIATE_REPR': IR, + 'VAR_SEMANTIC_ANALYSIS': SA, + }), + ); + final stacCode = sduiCode?['STAC']?.toString(); + if (stacCode == null) { + return null; + } + + return sduiCode['STAC'].toString(); +} + +Future modifySDUICodeUsingPrompt({ + required WidgetRef ref, + required String generatedSDUI, + required String modificationRequest, +}) async { + final res = await APIDashAgentCaller.instance.call( + StacModifierBot(), + ref: ref, + input: AgentInputs(variables: { + 'VAR_CODE': generatedSDUI, + 'VAR_CLIENT_REQUEST': modificationRequest, + }), + ); + final SDUI = res?['STAC']; + return SDUI; +} + +Future generateAPIToolUsingRequestData({ + required WidgetRef ref, + required String requestData, + required String targetLanguage, + required String selectedAgent, +}) async { + final toolfuncRes = await APIDashAgentCaller.instance.call( + APIToolFunctionGenerator(), + ref: ref, + input: AgentInputs(variables: { + 'REQDATA': requestData, + 'TARGET_LANGUAGE': targetLanguage, + }), + ); + if (toolfuncRes == null) { + return null; + } + + String toolCode = toolfuncRes!['FUNC']; + + final toolres = await APIDashAgentCaller.instance.call( + ApiToolBodyGen(), + ref: ref, + input: AgentInputs(variables: { + 'TEMPLATE': + APIToolGenTemplateSelector.getTemplate(targetLanguage, selectedAgent) + .substitutePromptVariable('FUNC', toolCode), + }), + ); + if (toolres == null) { + return null; + } + String toolDefinition = toolres!['TOOL']; + return toolDefinition; +} diff --git a/lib/templates/rulesets/stac_ruleset.dart b/lib/templates/rulesets/stac_ruleset.dart new file mode 100644 index 00000000..d32df1a0 --- /dev/null +++ b/lib/templates/rulesets/stac_ruleset.dart @@ -0,0 +1,329 @@ +const kRulesetStac = """ +### Scaffold +``` +{ + "type": "scaffold", + "appBar": { + "type": "appBar", + "title": { + "type": "text", + "data": "App Bar Title" + } + }, + "body": {}, + "backgroundColor": "#FFFFFF" +} +``` +--- +### Align +``` +{ + "type": "align", + "alignment": "topEnd", + "child": {...} +} +``` +--- +### Card +``` +{ + "type": "card", + "color": "#FFFFFF", + "shadowColor": "#000000", + "surfaceTintColor": "#FF0000", + "elevation": 5.0, + "shape": { + "type": "roundedRectangle", + "borderRadius": 10.0 + }, + "borderOnForeground": true, + "margin": { + "left": 10, + "top": 20, + "right": 10, + "bottom": 20 + }, + "clipBehavior": "antiAlias", + "child": {}, + "semanticContainer": true +} +``` +--- +### Center +``` +{ + "type": "center", + "child": { + "type": "text", + "data": "Hello, World!" + } +} +``` +--- +### Circle Avatar +``` +{ + "type": "circleAvatar", + "backgroundColor": "#FF0000", + "foregroundColor": "#FFFFFF", + "backgroundImage": "https://raw.githubusercontent.com/StacDev/stac/refs/heads/dev/assets/companies/bettrdo.jpg", + "radius": 50, + "child": { + "type": "text", + "data": "A" + } +} +``` +--- +### Column +``` +{ + "type": "column", + "mainAxisAlignment": "center", + "crossAxisAlignment": "start", + "mainAxisSize": "min", + "verticalDirection": "up", + "spacing": 10, + "children": [ + { + "type": "text", + "data": "Hello, World!" + }, + { + "type": "container", + "width": 100, + "height": 100, + "color": "#FF0000" + } + ] +} +``` +--- +### Container +``` +{ + "type": "container", + "alignment": "center", + "padding": { + "top": 16.0, + "bottom": 16.0, + "left": 16.0, + "right": 16.0 + }, + "decoration": { + "color": "#FF5733", + "borderRadius": { + "topLeft": 16.0, + "topRight": 16.0, + "bottomLeft": 16.0, + "bottomRight": 16.0 + } + }, + "width": 200.0, + "height": 200.0, + "child": { + "type": "text", + "data": "Hello, World!", + "style": { + "color": "#FFFFFF", + "fontSize": 24.0 + } + } +} +``` +--- +### GridView +``` +{ + "type": "gridView", + "physics": "never", + "shrinkWrap": true, + "padding": { + "left": 10, + "top": 10, + "right": 10, + "bottom": 10 + }, + "crossAxisCount": 2, + "mainAxisSpacing": 10.0, + "crossAxisSpacing": 10.0, + "children": [ + { + "type": "text", + "data": "Item 1" + }, + { + "type": "text", + "data": "Item 2" + } + ], +} +``` +--- +### Icon +``` +{ + "type": "icon", + "icon": "home", + "size": 24.0, + "color": "#000000", + "semanticLabel": "Home Icon", + "textDirection": "ltr" +} +``` +--- +### Image +``` +{ + "type": "image", + "src": "https://example.com/image.png", + "alignment": "center", + "imageType": "network", + "color": "#FFFFFF", + "width": 200.0, + "height": 100.0, + "fit": "contain" +} +``` +--- +### ListTile +``` +{ + "type": "listTile", + "leading": { + "type": "image", + "src": "https://cdn-icons-png.flaticon.com/512/3135/3135715.png" + }, + "title": {}, + "subtitle": {}, + "trailing": {} +} +``` +--- +### Padding +``` +{ + "type": "padding", + "padding": { + "top": 80, + "left": 24, + "right": 24, + "bottom": 24 + }, + "child": {...} +} +``` +--- +### Row +``` +{ + "type": "row", + "mainAxisAlignment": "center", + "crossAxisAlignment": "center", + "spacing": 12, + "children": [] +} +``` +--- +### SingleChildScrollView +``` +{ + "type": "singleChildScrollView", + "child": { + "type": "column", + "children": [ + + ] + } +} +``` +--- +### SizedBox +``` +{ + "type": "sizedBox", + "height": 25 +} +{ + "type": "sizedBox", + "width": 25 +} +``` +--- +### Table +``` +{ + "type": "table", + "columnWidths": { + "1": { "type": "fixedColumnWidth", "value": 200 } + }, + "defaultColumnWidth": { "type": "flexColumnWidth", "value": 1 }, + "textDirection": "ltr", + "defaultVerticalAlignment": "bottom", + "border": { + "color": "#428AF5", + "width": 1.0, + "borderRadius": 16 + }, + "children": [ + { + "type": "tableRow", + "children": [ + { "type": "tableCell", "child": { "type": "text", "data": "Header 1" } }, + ] + }, + ] +} +``` +--- +### TableCell +``` +{ + "type": "tableCell", + "verticalAlignment": "top", + "child": { + "type": "container", + "color": "#40000000", + "height": 50.0, + "child": { + "type": "center", + "child": { + "type": "text", + "data": "Header 1" + } + } + } +} +``` + +## Stac Styles (Analogous to Flutter Styles) + +### Border Radius +``` +//implicit +{ + "borderRadius": 16.0 +} +//explicit +{ + "borderRadius": { + "topLeft": 16.0, + "topRight": 16.0, + "bottomLeft": 16.0, + "bottomRight": 16.0 + } +} +``` +--- +### Border +``` +{ + "border": { + "color": "#FF0000", + "width": 2.0, + "borderStyle": "solid", + "strokeAlign": 0.0 + } +} +``` +"""; diff --git a/lib/templates/system_prompt_templates/apitool_bodygen_prompt.dart b/lib/templates/system_prompt_templates/apitool_bodygen_prompt.dart new file mode 100644 index 00000000..bc4f6640 --- /dev/null +++ b/lib/templates/system_prompt_templates/apitool_bodygen_prompt.dart @@ -0,0 +1,29 @@ +const String kPromptAPIToolBodyGen = """ +You are an expert API Tool Format Corrector Agent + +An API tool is a predefined or dynamically generated interface that the AI can call to perform specific external actions—such as fetching data, executing computations, or triggering real-world services—through an Application Programming Interface (API). + +You will be provided with a partially complete API tool template that will contain the api calling function named func and the tool definition +Your job is to correct any mistakes and provide the correct output. + +The template will contain the following variables (A Variable is marked by :: +Wherever you find this pattern replace it with the appropriate values) +`TOOL_NAME`: The name of the API Tool, infer it from the function code +`TOOL_DESCRIPTION`: The Description of the Tool, generate it based on the tool name +`TOOL_PARAMS`: The example of parameters have been provided below, infer the parameters needed from the func body, it must be a dictionary +`REQUIRED_PARAM_NAMES`: infer what parameters are required and add thier names in a list +`INPUT_SCHEMA`: if this variable exists, then create a StructuredTool or DynamicStructuredTool schema of the input according to the language of the tool itself. + +this is the general format of parameters: +"ARG_NAME": { + "type": "ARG_TYPE", + "description: "ARG_DESC" +} + +ALWAYS return the output as code only and do not start or begin with any introduction or conclusion. ONLY THE CODE. + +Here's the Template: +``` +:TEMPLATE: +``` +"""; diff --git a/lib/templates/system_prompt_templates/apitool_funcgen_prompt.dart b/lib/templates/system_prompt_templates/apitool_funcgen_prompt.dart new file mode 100644 index 00000000..e5dc2a2e --- /dev/null +++ b/lib/templates/system_prompt_templates/apitool_funcgen_prompt.dart @@ -0,0 +1,38 @@ +const String kPromptAPIToolFuncGen = """ +You are an expert LANGUAGE-SPECIFIC API CALL METHOD generator. + +You will always be provided with: +1. (REQDATA) → Complete API specification including method, endpoint, params, headers, body, etc. +2. (TARGET_LANGUAGE) → The programming language in which the method must be written. + +Your task: +- Generate a single method **explicitly named `func`** in the target language. +- The method must accept all dynamic variables (from query params, path params, request body fields, etc.) as function arguments. +- Embed all fixed/static values from REQDATA (e.g., Authorization tokens, fixed headers, constant body fields) directly inside the method. Do **not** expect them to be passed as arguments. + +Strict rules: +1. **No extra output** — only return the code for the function `func`, nothing else. +2. **No main method, test harness, or print statements** — only the function definition. +3. **Headers & Authorization**: + - If REQDATA specifies headers (including `Authorization`), hardcode them inside the method. + - Never expose these as parameters unless explicitly marked as variable in REQDATA. +4. **Request Body Handling**: + - If `REQDATA.BODY_TYPE == TEXT`: send the raw text as-is. + - If `REQDATA.BODY_TYPE == JSON` or `FORM-DATA`: create function arguments for the variable fields and serialize them according to best practices in the target language. +5. **Parameters**: + - Query params and path params must be represented as function arguments. + - Ensure correct encoding/escaping as per target language conventions. +6. **Error Handling**: + - Implement minimal, idiomatic error handling for the target language (e.g., try/except, promise rejection handling). +7. **Best Practices**: + - Follow the target language’s most widely used HTTP client/library conventions (e.g., `requests` in Python, `fetch`/`axios` in JavaScript, `http.Client` in Go). + - Keep the function minimal, clean, and production-ready. + +Inputs: +REQDATA: :REQDATA: +TARGET_LANGUAGE: :TARGET_LANGUAGE: + +Output: +- ONLY the function definition named `func` in the target language. +- Do not add explanations, comments, or surrounding text. Code only. +"""; diff --git a/lib/templates/system_prompt_templates/intermediate_rep_gen_prompt.dart b/lib/templates/system_prompt_templates/intermediate_rep_gen_prompt.dart new file mode 100644 index 00000000..9d61f345 --- /dev/null +++ b/lib/templates/system_prompt_templates/intermediate_rep_gen_prompt.dart @@ -0,0 +1,55 @@ +const String kPromptIntermediateRepGen = """ +You are an expert UI architect specializing in converting structured API responses into high-quality user interface designs. + +Your task is to analyze the given API response (`API_RESPONSE`) and return a **UI schema** in a clean, human-readable **Markdown format**. This schema will later be used by another system to generate the actual UI. + +### Your Output Must: +- Be in structured Markdown format (no Flutter code or JSON) +- Represent a layout hierarchy using indentation +- Only use the following allowed UI elements (Flutter-based): + - Text + - Row, Column + - GridView, SingleChildScrollView, Expanded + - Image + - ElevatedButton + - Icon + - Padding, SizedBox, Card, Container, Spacer, ListTile + - Table + +### Guidelines: +- Pick the best layout based on the structure and type of data +- Use rows/columns/tables where appropriate +- Use Cards to group related info +- Add short labels to explain each component's purpose +- Only use allowed elements — no custom widgets or other components +- if there are actual image links in the incoming data, please use them + +You must **include alignment information** where relevant, using the following format: +[ElementType] Label (alignment: ..., mainAxis: ..., crossAxis: ...) + +### Example Markdown Schema: +``` +- **[Column] Root layout** *(mainAxis: start, crossAxis: stretch)* + - **[Card] Match Info** + - **[Text]** "India vs Australia" *(alignment: centerLeft)* + - **[Text]** "Date: Aug 15, 2025" *(alignment: centerLeft)* + - **[Row] Pagination Info** *(mainAxis: spaceBetween, crossAxis: center)* + - **[Text]** "Page: 1" + - **[Text]** "Total: 12" + - **[ListView] User Cards** *(scrollDirection: vertical)* + - **[Card] User Item (George)** + - **[Row] Avatar and Info** *(mainAxis: start, crossAxis: center)* + - **[Image]** Avatar *(alignment: center, fit: cover)* + - **[Column] User Info** *(mainAxis: start, crossAxis: start)* + - **[Text]** Name: George Bluth + - **[Text]** Email: george@example.com +``` + +# Inputs +API_RESPONSE: ```json +:VAR_API_RESPONSE: +``` + +Return only the Schema and nothing else and MAKE SURE TO USE the Actual VALUES instead of text placeholders. this is very important +If you notice the content is too long then please include a Single Child Scroll Viewbut make sure you are handing cases wherein multiple scroll views are used and stuff + """; diff --git a/lib/templates/system_prompt_templates/semantic_analyser_prompt.dart b/lib/templates/system_prompt_templates/semantic_analyser_prompt.dart new file mode 100644 index 00000000..2bc10a21 --- /dev/null +++ b/lib/templates/system_prompt_templates/semantic_analyser_prompt.dart @@ -0,0 +1,15 @@ +const String kPromptSemanticAnalyser = """ +You are an expert at understanding and semantically interpreting JSON API responses. When provided with a sample API response in JSON format, your task is to produce a clear and concise semantic analysis that identifies the core data structures, their meaning, and what parts are relevant for a user interface. + +Your output must be in **plain text only** — no markdown, no formatting, no lists — just a single well-structured paragraph. This paragraph will be fed into a separate UI generation system, so it must be tailored accordingly. + +Focus only on the fields and data structures that are useful for generating a UI. Omit or instruct to ignore fields that are irrelevant for display purposes (e.g., metadata, internal identifiers, pagination if not needed visually, etc.). + +Describe: +- What the data represents (e.g., a list of users, product details, etc.) +- What UI elements or components would be ideal to display this data (e.g., cards, tables, images, lists) +- Which fields should be highlighted or emphasized +- Any structural or layout suggestions that would help a UI builder understand how to present the information + +Do **not** use formatting of any kind. Do **not** start or end the response with any extra commentary or boilerplate. Just return the pure semantic explanation of the data in a clean paragraph, ready for use by another LLM. + """; diff --git a/lib/templates/system_prompt_templates/stac_gen_prompt.dart b/lib/templates/system_prompt_templates/stac_gen_prompt.dart new file mode 100644 index 00000000..7d5c0a6c --- /dev/null +++ b/lib/templates/system_prompt_templates/stac_gen_prompt.dart @@ -0,0 +1,59 @@ +import '../rulesets/stac_ruleset.dart'; + +const String kPromptStacGen = """ +You are an expert agent whose one and only task is to generate Server Driven UI Code (json-like) representation from the given inputs. + +You will be provided with the Rules of the SDUI language, schema, text description as follows: + +SDUI CODE RULES: +( +$kRulesetStac +) + +DO NOT CREATE YOUR OWN SYNTAX. ONLY USE WHAT IS PROVIDED BY THE ABOVE RULES + +# Style/Formatting Rules +- No trailing commas. No comments. No undefined props. +- Strings for enums like mainAxisAlignment: "center". +- padding/margin objects may include any of: left,right,top,bottom,all,horizontal,vertical. +- style objects are opaque key-value maps (e.g., in text.style, elevatedButton.style); include only needed keys. + +#Behavior Conventions +- Use sizedBox for minor spacing; spacer/expanded for flexible space. +- Use listView for long, homogeneous lists; column for short static stacks. +- Always ensure images have at least src; add fit if necessary (e.g., "cover"). +- Prefer card for grouped content with elevation. +- Use gridView only if there are 2+ columns of similar items. + +# Validation Checklist (apply before emitting) +- Widgets/props only from catalog. +- All required props present (type, leaf essentials like text.data, image.src). +- Property types correct; no nulls/empties. +- Keys ordered deterministically. + +# Inputs +SCHEMA: ```:VAR_INTERMEDIATE_REPR:``` +DESCRIPTION: ```:VAR_SEMANTIC_ANALYSIS:``` + +# Generation Steps (follow silently) +- Read SCHEMA to identify concrete entities/IDs; read DESCRIPTION for layout intent. +- Pick widgets from the catalog that best express the layout. +- Compose from coarse to fine: page → sections → rows/columns → leaf widgets. +- Apply sensible defaults (alignment, spacing) only when needed. +- Validate: catalog-only widgets/props, property types, no unused fields, deterministic ordering. + +# Hard Output Contract +- Output MUST be ONLY the SDUI JSON. No prose, no code fences, no comments. Must start with { and end with }. +- Use only widgets and properties from the Widget Catalog below. +- Prefer minimal, valid trees. Omit null/empty props. +- Numeric where numeric, booleans where booleans, strings for enums/keys. +- Color strings allowed (e.g., "#RRGGBB"). +- Keep key order consistent: type, then layout/meta props, then child/children. + +# Final Instruction +Using SCHEMA and DESCRIPTION, output only the SDUI JSON that satisfies the rules above. DO NOT START OR END THE RESPONSE WITH ANYTHING ELSE. + +if there are no scrollable elements then wrap the whole content with a single child scroll view, if there are scrollable contents inside, then apply shrinkWrap and handle accordingly like +you would do in Flutter but in this Stac Representation + +"""; diff --git a/lib/templates/system_prompt_templates/stac_modifier_prompt.dart b/lib/templates/system_prompt_templates/stac_modifier_prompt.dart new file mode 100644 index 00000000..33c60ccc --- /dev/null +++ b/lib/templates/system_prompt_templates/stac_modifier_prompt.dart @@ -0,0 +1,28 @@ +import '../rulesets/stac_ruleset.dart'; + +const String kPromptStacModifier = """ +You are an expert agent whose sole JOB is to accept FLutter-SDUI (json-like) representation +and modify it to match the requests of the client. + +SDUI CODE RULES: +$kRulesetStac + +# Inputs +PREVIOUS_CODE: ```:VAR_CODE:``` +CLIENT_REQUEST: ```:VAR_CLIENT_REQUEST:``` + + +# Hard Output Contract +- Output MUST be ONLY the SDUI JSON. No prose, no code fences, no comments. Must start with { and end with }. +- Use only widgets and properties from the Widget Catalog below. +- Prefer minimal, valid trees. Omit null/empty props. +- Numeric where numeric, booleans where booleans, strings for enums/keys. +- Color strings allowed (e.g., "#RRGGBB"). +- Keep key order consistent: type, then layout/meta props, then child/children. + + +# Final Instruction +DO NOT CHANGE ANYTHING UNLESS SPECIFICALLY ASKED TO +use the CLIENT_REQUEST to modify the PREVIOUS_CODE while following the existing FLutter-SDUI (json-like) representation +ONLY FLutter-SDUI Representation NOTHING ELSE. DO NOT START OR END WITH TEXT, ONLY FLutter-SDUI Representatiin. +"""; diff --git a/lib/templates/system_prompt_templates/stac_to_flutter_prompt.dart b/lib/templates/system_prompt_templates/stac_to_flutter_prompt.dart new file mode 100644 index 00000000..d2d7b30c --- /dev/null +++ b/lib/templates/system_prompt_templates/stac_to_flutter_prompt.dart @@ -0,0 +1,15 @@ +const String kPromptStacToFlutter = """ +You are an expert agent whose sole JOB is to accept FLutter-SDUI (json-like) representation +and convert it into actual working FLutter component. + +This is fairly easy to do as FLutter-SDUI is literally a one-one representation of Flutter Code + +SDUI_CODE: ```:VAR_CODE:``` + +use the Above SDUI_CODE and convert it into Flutter Code that is effectively same as what the SDUI_CODE represents + +DO NOT CHANGE CONTENT, just convert everything one-by-one +Output ONLY Code Representation NOTHING ELSE. DO NOT START OR END WITH TEXT, ONLY Code + +DO NOT WRITE CODE TO PARSE SDUI, ACTUALLY CONVERT IT TO REAL DART CODE +"""; diff --git a/lib/templates/templates.dart b/lib/templates/templates.dart new file mode 100644 index 00000000..9df768d8 --- /dev/null +++ b/lib/templates/templates.dart @@ -0,0 +1,8 @@ +export 'system_prompt_templates/apitool_bodygen_prompt.dart'; +export 'system_prompt_templates/apitool_funcgen_prompt.dart'; +export 'system_prompt_templates/intermediate_rep_gen_prompt.dart'; +export 'system_prompt_templates/semantic_analyser_prompt.dart'; +export 'system_prompt_templates/stac_to_flutter_prompt.dart'; +export 'system_prompt_templates/stac_gen_prompt.dart'; +export 'system_prompt_templates/stac_modifier_prompt.dart'; +export 'tool_templates.dart'; diff --git a/lib/templates/tool_templates.dart b/lib/templates/tool_templates.dart new file mode 100644 index 00000000..3ecf0a3e --- /dev/null +++ b/lib/templates/tool_templates.dart @@ -0,0 +1,112 @@ +const GENERAL_ARG_PROPERTY_FORMAT_PY = """:ARG_NAME: { + "type": ":ARG_TYPE:", + "description: ":ARG_DESC:" +}"""; + +const GENERAL_PYTHON_TOOL_FORMAT = """ +:FUNC: + +api_tool = { + "function": func, + "definition": { + "name": ":TOOL_NAME:", + "description": ":TOOL_DESCRIPTION:", + "parameters": { + "type": "object", + "properties": :TOOL_PARAMS:, + "required": [:REQUIRED_PARAM_NAMES:], + "additionalProperties": False + } + } +} + +__all__ = ["api_tool"] +"""; + +const GENERAL_JAVASCRIPT_TOOL_FORMAT = """ +:FUNC: + +const apiTool = { + function: func, + definition: { + type: 'function', + function: { + name: ':TOOL_NAME:', + description: ':TOOL_DESCRIPTION:', + parameters: { + type: 'object', + properties: :TOOL_PARAMS:, + required: [:REQUIRED_PARAM_NAMES:] + additionalProperties: false + } + } + } +}; + +export { apiTool }; +"""; + +const LANGCHAIN_PYTHON_TOOL_FORMAT = """ +from langchain.tools import StructuredTool + +:INPUT_SCHEMA: + +:FUNC: + +api_tool = StructuredTool.from_function( + func=func, + name=":TOOL_NAME:", + description=":TOOL_DESCRIPTION:", + args_schema=INPUT_SCHEMA, +) +__all__ = ["api_tool"] +"""; + +const LANGCHAIN_JAVASCRIPT_TOOL_FORMAT = """ +import { DynamicStructuredTool } from 'langchain/tools'; +import { z } from 'zod'; + +:INPUT_SCHEMA: + +:FUNC: + +const apiTool = new DynamicStructuredTool({ + func: func, + name: ':TOOL_NAME:', + description: ':TOOL_DESCRIPTION:', + schema: INPUT_SCHEMA +}); + +export { apiTool }; +"""; + +const MICROSOFT_AUTOGEN_TOOL_FORMAT = """ +:FUNC: + +api_tool = { + "function": func, + "name": ":TOOL_NAME:", + "description": ":TOOL_DESCRIPTION:" +} + +__all__ = ["api_tool"] +"""; + +class APIToolGenTemplateSelector { + static String getTemplate(String language, String agent) { + if (language == 'PYTHON') { + if (agent == 'MICROSOFT_AUTOGEN') { + return MICROSOFT_AUTOGEN_TOOL_FORMAT; + } else if (agent == 'LANGCHAIN') { + return LANGCHAIN_PYTHON_TOOL_FORMAT; + } + return GENERAL_PYTHON_TOOL_FORMAT; + } else if (language == 'JAVASCRIPT') { + if (agent == 'LANGCHAIN') { + return LANGCHAIN_JAVASCRIPT_TOOL_FORMAT; + } + return GENERAL_JAVASCRIPT_TOOL_FORMAT; + } + return 'NO_TEMPLATE'; + } +} diff --git a/lib/widgets/response_body.dart b/lib/widgets/response_body.dart index f1c866e9..090fdcbb 100644 --- a/lib/widgets/response_body.dart +++ b/lib/widgets/response_body.dart @@ -10,9 +10,11 @@ class ResponseBody extends StatelessWidget { const ResponseBody({ super.key, this.selectedRequestModel, + this.isPartOfHistory = false, }); final RequestModel? selectedRequestModel; + final bool isPartOfHistory; @override Widget build(BuildContext context) { @@ -22,9 +24,8 @@ class ResponseBody extends StatelessWidget { message: '$kNullResponseModelError $kUnexpectedRaiseIssue'); } - final isSSE = responseModel.sseOutput?.isNotEmpty ?? false; var body = responseModel.body; - var formattedBody = responseModel.formattedBody; + if (body == null) { return const ErrorMessage( message: '$kMsgNullBody $kUnexpectedRaiseIssue'); @@ -36,9 +37,6 @@ class ResponseBody extends StatelessWidget { showIssueButton: false, ); } - if (isSSE) { - body = responseModel.sseOutput!.join(); - } final mediaType = responseModel.mediaType ?? MediaType(kTypeText, kSubTypePlain); @@ -55,6 +53,11 @@ class ResponseBody extends StatelessWidget { var options = responseBodyView.$1; var highlightLanguage = responseBodyView.$2; + final isSSE = responseModel.sseOutput?.isNotEmpty ?? false; + var formattedBody = isSSE + ? responseModel.sseOutput!.join('\n') + : responseModel.formattedBody; + if (formattedBody == null) { options = [...options]; options.remove(ResponseBodyView.code); @@ -71,6 +74,7 @@ class ResponseBody extends StatelessWidget { sseOutput: responseModel.sseOutput, isAIResponse: selectedRequestModel?.apiType == APIType.ai, aiRequestModel: selectedRequestModel?.aiRequestModel, + isPartOfHistory: isPartOfHistory, ); } } diff --git a/lib/widgets/response_body_success.dart b/lib/widgets/response_body_success.dart index 44c9b28a..a120f1e4 100644 --- a/lib/widgets/response_body_success.dart +++ b/lib/widgets/response_body_success.dart @@ -1,3 +1,5 @@ +import 'package:apidash/screens/common_widgets/agentic_ui_features/ai_ui_designer/generate_ui_dialog.dart'; +import 'package:apidash/screens/common_widgets/agentic_ui_features/tool_generation/generate_tool_dialog.dart'; import 'package:apidash_core/apidash_core.dart'; import 'package:apidash_design_system/apidash_design_system.dart'; import 'package:flutter/foundation.dart'; @@ -19,6 +21,7 @@ class ResponseBodySuccess extends StatefulWidget { this.sseOutput, this.isAIResponse = false, this.aiRequestModel, + this.isPartOfHistory = false, }); final MediaType mediaType; final List options; @@ -29,6 +32,7 @@ class ResponseBodySuccess extends StatefulWidget { final String? highlightLanguage; final bool isAIResponse; final AIRequestModel? aiRequestModel; + final bool isPartOfHistory; @override State createState() => _ResponseBodySuccessState(); @@ -61,6 +65,16 @@ class _ResponseBodySuccessState extends State { padding: kP10, child: Column( children: [ + if (!widget.isPartOfHistory) + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Expanded(child: GenerateToolButton()), + SizedBox(width: 10), + Expanded(child: AIGenerateUIButton()), + ], + ), + kVSpacer10, Row( children: [ (widget.options == kRawBodyViewOptions) diff --git a/lib/widgets/widget_sending.dart b/lib/widgets/widget_sending.dart index 8c949061..d50b7318 100644 --- a/lib/widgets/widget_sending.dart +++ b/lib/widgets/widget_sending.dart @@ -7,9 +7,11 @@ import 'package:apidash/consts.dart'; class SendingWidget extends StatefulWidget { final DateTime? startSendingTime; + final bool showTimeElapsed; const SendingWidget({ super.key, required this.startSendingTime, + this.showTimeElapsed = true, }); @override @@ -51,33 +53,34 @@ class _SendingWidgetState extends State { Center( child: Lottie.asset(kAssetSendingLottie), ), - Padding( - padding: kPh20t40, - child: Visibility( - visible: _millisecondsElapsed >= 0, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.alarm, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - const SizedBox( - width: 10, - ), - Text( - 'Time elapsed: ${humanizeDuration(Duration(milliseconds: _millisecondsElapsed))}', - textAlign: TextAlign.center, - overflow: TextOverflow.fade, - softWrap: false, - style: kTextStyleButton.copyWith( + if (widget.showTimeElapsed) + Padding( + padding: kPh20t40, + child: Visibility( + visible: _millisecondsElapsed >= 0, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.alarm, color: Theme.of(context).colorScheme.onSurfaceVariant, ), - ), - ], + const SizedBox( + width: 10, + ), + Text( + 'Time elapsed: ${humanizeDuration(Duration(milliseconds: _millisecondsElapsed))}', + textAlign: TextAlign.center, + overflow: TextOverflow.fade, + softWrap: false, + style: kTextStyleButton.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + ), ), ), - ), ], ); } diff --git a/packages/better_networking/CHANGELOG.md b/packages/better_networking/CHANGELOG.md index 59f048f2..23df4aec 100644 --- a/packages/better_networking/CHANGELOG.md +++ b/packages/better_networking/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.0.2 + +- Improvement of various core features. + ## 0.0.1 -* Intial release. +- Intial release. diff --git a/packages/better_networking/better_networking_example/pubspec.lock b/packages/better_networking/better_networking_example/pubspec.lock index 5951bb22..a703cb1d 100644 --- a/packages/better_networking/better_networking_example/pubspec.lock +++ b/packages/better_networking/better_networking_example/pubspec.lock @@ -23,7 +23,7 @@ packages: path: ".." relative: true source: path - version: "0.0.1" + version: "0.0.2" boolean_selector: dependency: transitive description: diff --git a/packages/better_networking/pubspec.yaml b/packages/better_networking/pubspec.yaml index 50caa33f..91c7b123 100644 --- a/packages/better_networking/pubspec.yaml +++ b/packages/better_networking/pubspec.yaml @@ -1,6 +1,6 @@ name: better_networking description: "Simplified Networking. Support for sending REST & GraphQL API Requests." -version: 0.0.1 +version: 0.0.2 homepage: https://github.com/foss42/apidash topics: diff --git a/packages/genai/.pubignore b/packages/genai/.pubignore new file mode 100644 index 00000000..8f49ea12 --- /dev/null +++ b/packages/genai/.pubignore @@ -0,0 +1,12 @@ +pubspec.lock +melos_genai.iml +build/ +coverage/ +dart_test.yaml +doc/ +test/ +pubspec_overrides.yaml +genai_example/melos_genai_example.iml +genai_example/pubspec_overrides.yaml +tool/ +models.json diff --git a/packages/genai/CHANGELOG.md b/packages/genai/CHANGELOG.md index 41cc7d81..4ebb3af0 100644 --- a/packages/genai/CHANGELOG.md +++ b/packages/genai/CHANGELOG.md @@ -1,3 +1,3 @@ ## 0.0.1 -* TODO: Describe initial release. +- Introducing a unified Dart/Flutter package for working with multiple Generative AI providers (Google Gemini, OpenAI, Anthropic, Azure OpenAI, Ollama, etc.). diff --git a/packages/genai/README.md b/packages/genai/README.md index 92f4d126..6d43b408 100644 --- a/packages/genai/README.md +++ b/packages/genai/README.md @@ -2,11 +2,11 @@ A **unified Dart/Flutter package** for working with multiple Generative AI providers (Google Gemini, OpenAI, Anthropic, Azure OpenAI, Ollama, etc.) using a **single request model**. -* ✅ Supports **normal & streaming** responses -* ✅ Unified `AIRequestModel` across providers -* ✅ Configurable parameters (temperature, top-p, max tokens, etc.) -* ✅ Simple request utilities (`executeGenAIRequest`, `streamGenAIRequest`) -* ✅ Extensible — add your own provider easily +- ✅ Supports **normal & streaming** responses +- ✅ Unified `AIRequestModel` across providers +- ✅ Configurable parameters (temperature, top-p, max tokens, etc.) +- ✅ Simple request utilities (`executeGenAIRequest`, `streamGenAIRequest`) +- ✅ Extensible — add your own provider easily --- @@ -85,10 +85,10 @@ Each request accepts `modelConfigs` to fine-tune output. Available configs (defaults provided): -* `temperature` → controls randomness -* `top_p` / `topP` → nucleus sampling probability -* `max_tokens` / `maxOutputTokens` → maximum length of output -* `stream` → enables streaming +- `temperature` → controls randomness +- `top_p` / `topP` → nucleus sampling probability +- `max_tokens` / `maxOutputTokens` → maximum length of output +- `stream` → enables streaming Example: @@ -135,9 +135,9 @@ processGenAIStreamOutput( ## 🔒 Authentication -* **OpenAI / Anthropic / Azure OpenAI** → API key passed as HTTP header. -* **Gemini** → API key passed as query param `?key=YOUR_API_KEY`. -* **Ollama** → local server, no key required. +- **OpenAI / Anthropic / Azure OpenAI** → API key passed as HTTP header. +- **Gemini** → API key passed as query param `?key=YOUR_API_KEY`. +- **Ollama** → local server, no key required. Just set `apiKey` in your `AIRequestModel`. @@ -150,10 +150,11 @@ Want to add a new AI provider? 1. Extend `ModelProvider` 2. Implement: - * `defaultAIRequestModel` - * `createRequest()` - * `outputFormatter()` - * `streamOutputFormatter()` + - `defaultAIRequestModel` + - `createRequest()` + - `outputFormatter()` + - `streamOutputFormatter()` + 3. Register in `kModelProvidersMap` That’s it — it plugs into the same unified request flow. @@ -175,16 +176,16 @@ print(answer); --- - ## 🤝 Contributing We welcome contributions to the `genai` package! If you'd like to contribute, please fork the repository and submit a pull request. For major changes or new features, it's a good idea to open an issue first to discuss your ideas. -## Maintainer +## Maintainer(s) +- Ankit Mahato ([GitHub](https://github.com/animator), [LinkedIn](https://www.linkedin.com/in/ankitmahato/), [X](https://x.com/ankitmahato)) - Ashita Prasad ([GitHub](https://github.com/ashitaprasad), [LinkedIn](https://www.linkedin.com/in/ashitaprasad/), [X](https://x.com/ashitaprasad)) - Manas Hejmadi (contributor) ([GitHub](https://github.com/synapsecode)) ## License -This project is licensed under the [Apache License 2.0](https://github.com/foss42/apidash/blob/main/packages/genai/LICENSE). \ No newline at end of file +This project is licensed under the [Apache License 2.0](https://github.com/foss42/apidash/blob/main/packages/genai/LICENSE). diff --git a/packages/genai/lib/agentic_engine/agent_service.dart b/packages/genai/lib/agentic_engine/agent_service.dart new file mode 100644 index 00000000..d218ccd1 --- /dev/null +++ b/packages/genai/lib/agentic_engine/agent_service.dart @@ -0,0 +1,90 @@ +import 'package:genai/agentic_engine/blueprint.dart'; +import 'package:genai/genai.dart'; + +class AIAgentService { + static Future _call_provider({ + required AIRequestModel baseAIRequestObject, + required String systemPrompt, + required String input, + }) async { + final aiRequest = baseAIRequestObject.copyWith( + systemPrompt: systemPrompt, + userPrompt: input, + ); + return await executeGenAIRequest(aiRequest); + } + + static Future _orchestrator( + AIAgent agent, + AIRequestModel baseAIRequestObject, { + String? query, + Map? variables, + }) async { + String sP = agent.getSystemPrompt(); + + //Perform Templating + if (variables != null) { + for (final v in variables.keys) { + sP = sP.substitutePromptVariable(v, variables[v]); + } + } + + return await _call_provider( + systemPrompt: sP, + input: query ?? '', + baseAIRequestObject: baseAIRequestObject, + ); + } + + static Future _governor( + AIAgent agent, + AIRequestModel baseAIRequestObject, { + String? query, + Map? variables, + }) async { + int RETRY_COUNT = 0; + List backoffDelays = [200, 400, 800, 1600, 3200]; + do { + try { + final res = await _orchestrator( + agent, + baseAIRequestObject, + query: query, + variables: variables, + ); + if (res != null) { + if (await agent.validator(res)) { + return agent.outputFormatter(res); + } + } + } catch (e) { + "AIAgentService::Governor: Exception Occured: $e"; + } + // Exponential Backoff + if (RETRY_COUNT < backoffDelays.length) { + await Future.delayed( + Duration(milliseconds: backoffDelays[RETRY_COUNT]), + ); + } + RETRY_COUNT += 1; + print( + "Retrying AgentCall for (${agent.agentName}): ATTEMPT: $RETRY_COUNT", + ); + } while (RETRY_COUNT < 5); + return null; + } + + static Future callAgent( + AIAgent agent, + AIRequestModel baseAIRequestObject, { + String? query, + Map? variables, + }) async { + return await _governor( + agent, + baseAIRequestObject, + query: query, + variables: variables, + ); + } +} diff --git a/packages/genai/lib/agentic_engine/agentic_engine.dart b/packages/genai/lib/agentic_engine/agentic_engine.dart new file mode 100644 index 00000000..abc52f3b --- /dev/null +++ b/packages/genai/lib/agentic_engine/agentic_engine.dart @@ -0,0 +1,2 @@ +export 'agent_service.dart'; +export 'blueprint.dart'; diff --git a/packages/genai/lib/agentic_engine/blueprint.dart b/packages/genai/lib/agentic_engine/blueprint.dart new file mode 100644 index 00000000..14b9bfc1 --- /dev/null +++ b/packages/genai/lib/agentic_engine/blueprint.dart @@ -0,0 +1,18 @@ +abstract class AIAgent { + String get agentName; + String getSystemPrompt(); + Future validator(String aiResponse); + Future outputFormatter(String validatedResponse); +} + +extension SystemPromptTemplating on String { + String substitutePromptVariable(String variable, String value) { + return this.replaceAll(":$variable:", value); + } +} + +class AgentInputs { + final String? query; + final Map? variables; + AgentInputs({this.query, this.variables}); +} diff --git a/packages/genai/lib/genai.dart b/packages/genai/lib/genai.dart index 7d58af8d..9332e59a 100644 --- a/packages/genai/lib/genai.dart +++ b/packages/genai/lib/genai.dart @@ -2,3 +2,4 @@ export 'models/models.dart'; export 'interface/interface.dart'; export 'utils/utils.dart'; export 'widgets/widgets.dart'; +export 'agentic_engine/agentic_engine.dart'; diff --git a/packages/genai/pubspec.yaml b/packages/genai/pubspec.yaml index 13237382..9f3813d0 100644 --- a/packages/genai/pubspec.yaml +++ b/packages/genai/pubspec.yaml @@ -1,8 +1,14 @@ name: genai -description: "Generative AI capabilities for flutter applications" +description: "A unified Dart/Flutter package for working with multiple Generative AI providers (Google Gemini, OpenAI, Anthropic, Azure OpenAI, Ollama, etc.) using a single request model." version: 0.0.1 -homepage: -publish_to: none +homepage: https://github.com/foss42/apidash/tree/main/packages/genai + +topics: + - ai + - ollama + - gemini + - claude + - openai environment: sdk: ^3.8.0 @@ -11,8 +17,7 @@ environment: dependencies: flutter: sdk: flutter - better_networking: - path: ../better_networking + better_networking: ^0.0.2 freezed_annotation: ^2.4.1 json_annotation: ^4.9.0 nanoid: ^1.0.0 diff --git a/packages/genai/test/genai_test.dart b/packages/genai/test/genai_test.dart index e5371074..10046aca 100644 --- a/packages/genai/test/genai_test.dart +++ b/packages/genai/test/genai_test.dart @@ -1,5 +1,4 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:genai/genai.dart'; -void main() { -} +void main() {} diff --git a/pubspec.lock b/pubspec.lock index 26a38d72..09fc2e08 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -404,6 +404,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.5.0" + dio: + dependency: transitive + description: + name: dio + sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9" + url: "https://pub.dev" + source: hosted + version: "5.8.0+1" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78" + url: "https://pub.dev" + source: hosted + version: "2.1.1" ed25519_edwards: dependency: transitive description: @@ -689,7 +705,7 @@ packages: source: hosted version: "2.5.8" freezed_annotation: - dependency: transitive + dependency: "direct overridden" description: name: freezed_annotation sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 @@ -1021,6 +1037,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.1.1" + logger: + dependency: transitive + description: + name: logger + sha256: "55d6c23a6c15db14920e037fe7e0dc32e7cdaf3b64b4b25df2d541b5b6b81c0c" + url: "https://pub.dev" + source: hosted + version: "2.6.1" logging: dependency: transitive description: @@ -1664,6 +1688,22 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.0" + stac: + dependency: "direct main" + description: + name: stac + sha256: "9c24c0cb546ab04bf324c17451ad31181cb98e284a489f690a6f381b7a77e47a" + url: "https://pub.dev" + source: hosted + version: "0.11.0" + stac_framework: + dependency: transitive + description: + name: stac_framework + sha256: "54e1a14f01664443f3be3d158781c07e69ab0e04993495ddd0b6b1d68c84875b" + url: "https://pub.dev" + source: hosted + version: "0.3.0" stack_trace: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 868b4bc2..3d481637 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -66,6 +66,7 @@ dependencies: scrollable_positioned_list: ^0.3.8 share_plus: ^10.1.4 shared_preferences: ^2.5.2 + stac: ^0.11.0 url_launcher: ^6.2.5 uuid: ^4.5.0 vector_graphics_compiler: ^1.1.9+1 @@ -81,6 +82,7 @@ dependency_overrides: extended_text_field: ^16.0.0 pdf_widget_wrapper: ^1.0.4 web: ^1.1.1 + freezed_annotation: ^2.0.3 dev_dependencies: flutter_test: diff --git a/test/models/settings_model_test.dart b/test/models/settings_model_test.dart index 3a7827d5..0ea7746a 100644 --- a/test/models/settings_model_test.dart +++ b/test/models/settings_model_test.dart @@ -19,6 +19,7 @@ void main() { workspaceFolderPath: null, isSSLDisabled: true, isDashBotEnabled: true, + defaultAIModel: {"model": "llama"}, ); test('Testing toJson()', () { @@ -38,6 +39,7 @@ void main() { "workspaceFolderPath": null, "isSSLDisabled": true, "isDashBotEnabled": true, + "defaultAIModel": {"model": "llama"} }; expect(sm.toJson(), expectedResult); }); @@ -59,6 +61,7 @@ void main() { "workspaceFolderPath": null, "isSSLDisabled": true, "isDashBotEnabled": true, + "defaultAIModel": {"model": "llama"} }; expect(SettingsModel.fromJson(input), sm); }); @@ -77,6 +80,7 @@ void main() { historyRetentionPeriod: HistoryRetentionPeriod.oneWeek, isSSLDisabled: false, isDashBotEnabled: false, + defaultAIModel: {"model": "llama"}, ); expect( sm.copyWith( @@ -104,7 +108,10 @@ void main() { "historyRetentionPeriod": "oneWeek", "workspaceFolderPath": null, "isSSLDisabled": true, - "isDashBotEnabled": true + "isDashBotEnabled": true, + "defaultAIModel": { + "model": "llama" + } }'''; expect(sm.toString(), expectedResult); });