diff --git a/README.md b/README.md index a6faafa..fc7e51a 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,12 @@ My take to get privacy friendly, offline AI models accessible to as many people as possible. The cutting edge offline AI models such as [Segment Anything](https://ai.facebook.com/research/publications/segment-anything/), [LLaMA](https://github.com/facebookresearch/llama) and [Whisper](https://github.com/openai/whisper) have been optimised by many other developers in various forms and sizes. The small to medium sizes are the ones I'm interested in. +
+ +[Skip to App Preview](#app-preview) + +
+ ## Getting Started Unlike the majority of frontends for AI models, this project aims to be easier than even those with 1-click installers to launch a web app. @@ -38,7 +44,7 @@ Stats: mid-70s. Great memory. Always forgets my date of birth, yet somehow remem That's him 👇 -Shady's Daddy +Shady's Daddy ## Goal @@ -90,6 +96,14 @@ Q: Why the name Shady.ai? A: Because it's shady. It's shady because it's not using the internet. Wait, that doesn't make any sense. I'll think of something better. +## Verified Systems + +My daily driver: + +- macOS: Ventura - 13.3.1 (22E261) +- Chip: M1 +- Memory: 16 GB + ## Disclaimer This project is not affiliated with Facebook, OpenAI or any other company. This is a personal project made by a hobbyist. @@ -104,4 +118,6 @@ Feel free to open an issue or a pull request. I'm open to any suggestions. ## App Preview -![shadyai_eval_ok](https://user-images.githubusercontent.com/5500332/234497371-eeb5be33-f884-4a90-b977-0d5f162641da.gif) +### macOS + +Screenshot of ShadyAI for MacOS - Launchpad diff --git a/assets/macos_preview_0.png b/assets/macos_preview_0.png new file mode 100644 index 0000000..d9dc53b Binary files /dev/null and b/assets/macos_preview_0.png differ diff --git a/lefthook.yaml b/lefthook.yaml index c89c3ed..56d3d18 100644 --- a/lefthook.yaml +++ b/lefthook.yaml @@ -9,17 +9,11 @@ pre-push: flutter-test: root: shady_ai_flutter/ run: flutter test - fail_text: "Could not pass tests" + fail_text: "❌ Could not pass tests" flutter-analyze: root: "shady_ai_flutter/" run: | flutter analyze - flutter pub run dart_code_metrics:metrics analyze lib - flutter-check-unused: - root: shady_ai_flutter/ - run: | - flutter pub run dart_code_metrics:metrics check-unused-files lib --exclude="{/**.g.dart,/**.freezed.dart}" - flutter pub run dart_code_metrics:metrics check-unused-code lib --exclude="{/**.g.dart,/**.freezed.dart}" # Note: commit-msg hook takes a single parameter, # the name of the file that holds the proposed commit log message. @@ -38,13 +32,4 @@ pre-commit: commands: dart-fix: root: shady_ai_flutter/ - glob: '*.dart' # Find all .dart files - exclude: '*.{g,freezed}.dart' # Exclude generated dart files - run: dart fix --apply "{staged_files}" - stage_fixed: true # Stage the fixed files - dart-format: - root: shady_ai_flutter/ - glob: '*.dart' # Find all .dart files - exclude: '*.{g,freezed}.dart' # Exclude generated dart files - run: dart format "{staged_files}" # Format staged files - stage_fixed: true # Stage the fixed files + run: dart fix --apply diff --git a/shady_ai_flutter/.mason/bricks.json b/shady_ai_flutter/.mason/bricks.json new file mode 100644 index 0000000..c60b39f --- /dev/null +++ b/shady_ai_flutter/.mason/bricks.json @@ -0,0 +1 @@ +{"feature_brick":"/Users/brutalcoding/.mason-cache/hosted/registry.brickhub.dev/feature_brick_0.6.2"} \ No newline at end of file diff --git a/shady_ai_flutter/analysis_options.yaml b/shady_ai_flutter/analysis_options.yaml index 7c50634..b429f35 100644 --- a/shady_ai_flutter/analysis_options.yaml +++ b/shady_ai_flutter/analysis_options.yaml @@ -1,6 +1,18 @@ +include: package:flutter_lints/flutter.yaml + analyzer: + exclude: + - "**/*.g.dart" plugins: - - dart_code_metrics + - custom_lint + +custom_lint: + rules: + - missing_provider_scope: true + - always_specify_types: true + # Enforce new lines before a return statement + - newline_before_return: true + dart_code_metrics: metrics: @@ -9,6 +21,7 @@ dart_code_metrics: maximum-nesting-level: 5 metrics-exclude: - test/** + - generated/** rules: - avoid-dynamic - avoid-passing-async-when-sync-expected diff --git a/shady_ai_flutter/assets/dad_the_benchmark.png b/shady_ai_flutter/assets/images/dad_cutout.png similarity index 100% rename from shady_ai_flutter/assets/dad_the_benchmark.png rename to shady_ai_flutter/assets/images/dad_cutout.png diff --git a/shady_ai_flutter/assets/dad_the_benchmark_icon.png b/shady_ai_flutter/assets/images/dad_the_benchmark_icon.png similarity index 100% rename from shady_ai_flutter/assets/dad_the_benchmark_icon.png rename to shady_ai_flutter/assets/images/dad_the_benchmark_icon.png diff --git a/shady_ai_flutter/assets/macos_screenshot.png b/shady_ai_flutter/assets/images/macos_screenshot.png similarity index 100% rename from shady_ai_flutter/assets/macos_screenshot.png rename to shady_ai_flutter/assets/images/macos_screenshot.png diff --git a/shady_ai_flutter/assets/images/shady_app_icon.png b/shady_ai_flutter/assets/images/shady_app_icon.png new file mode 100644 index 0000000..cacd9c6 Binary files /dev/null and b/shady_ai_flutter/assets/images/shady_app_icon.png differ diff --git a/shady_ai_flutter/lib/chat/domain/chat_provider.dart b/shady_ai_flutter/lib/chat/domain/chat_provider.dart new file mode 100644 index 0000000..2be98e8 --- /dev/null +++ b/shady_ai_flutter/lib/chat/domain/chat_provider.dart @@ -0,0 +1,9 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +final chatProvider = StateNotifierProvider.autoDispose((ref) { + return ChatProvider(); +}); + +class ChatProvider extends StateNotifier { + ChatProvider() : super(0); +} diff --git a/shady_ai_flutter/lib/chat/presentation/chat_page.dart b/shady_ai_flutter/lib/chat/presentation/chat_page.dart new file mode 100644 index 0000000..99d986e --- /dev/null +++ b/shady_ai_flutter/lib/chat/presentation/chat_page.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +import '../domain/chat_provider.dart'; + +/// {@template chat_page} +/// A page that can be used to chat with AI. +/// {@endtemplate} +class ChatPage extends StatelessWidget { + /// {@macro chat_page} + const ChatPage({super.key}); + + @override + Widget build(BuildContext context) { + return const Scaffold( + key: Key('chat_page_scaffold'), + body: ChatPageBody(), + ); + } +} + +class ChatPageBody extends HookConsumerWidget { + const ChatPageBody({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final count = ref.watch(chatProvider); + return Text( + count.toString(), + ); + } +} diff --git a/shady_ai_flutter/lib/common/helpers/file_helper.dart b/shady_ai_flutter/lib/common/helpers/file_helper.dart new file mode 100644 index 0000000..65227d6 --- /dev/null +++ b/shady_ai_flutter/lib/common/helpers/file_helper.dart @@ -0,0 +1,111 @@ +// Load the model and evaluate it +// Returns true if the evaluation was successful +import 'dart:ffi'; +import 'dart:io'; + +import 'package:ffi/ffi.dart'; +import 'package:file_selector/file_selector.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:path/path.dart' as p; +import 'package:shady_ai/generated/rwkv/rwkv_bindings.g.dart'; + +Future openFileCheckpoint() async { + const XTypeGroup typeGroup = XTypeGroup( + label: 'model', + extensions: ['bin'], + ); + final XFile? file = + await openFile(acceptedTypeGroups: [typeGroup]); + + if (file == null) { + // Operation was canceled by the user. + return null; + } + + return file; +} + +// TODO(BrutalCoding): Finish implementing this +// ignore: unused_element +Future _loadModel(BuildContext context) async { + RWKV? rwkv; + Pointer? ctx; + try { + // Get path to AI model to infer + final fileOfAiModel = await openFileCheckpoint(); + if (fileOfAiModel == null) { + return false; + } + + // Create a temporary file and write the data to it + await fileOfAiModel.readAsBytes(); + + // Get basename of the file using the path + final fileNameOfYourModel = p.basename(fileOfAiModel.path); + + // Create a temporary file and write the data to it + final tempFile = File("${Directory.systemTemp.path}/$fileNameOfYourModel"); + + // await tempFile.writeAsBytes(data.buffer.asUint8List()); + final modelFilePath = tempFile.path; + + // Load the dynamic library + rwkv = RWKV(await loadLibrwkv()); + final modelFilePathCStr = modelFilePath.toNativeUtf8().cast(); + ctx = rwkv.rwkv_init_from_file(modelFilePathCStr, 1); + + final stateBuffer = rwkv.rwkv_get_state_buffer_element_count(ctx); + final Pointer stateBufferPtr = malloc.allocate(stateBuffer); + + final logitsBuffer = rwkv.rwkv_get_logits_buffer_element_count(ctx); + final Pointer logitsBufferPtr = malloc.allocate(logitsBuffer); + + // TODO(BrutalCoding): Get the token from the user (e.g. via a text field or calculate it from the input) + const token = 103; + + final result = rwkv.rwkv_eval( + ctx, + token, + stateBufferPtr, + stateBufferPtr, + logitsBufferPtr, + ); + + return result; + } catch (e) { + // Show an error message with ScaffoldMessenger + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error: $e'), + ), + ); + } + if (rwkv == null || ctx == null) return false; + rwkv.rwkv_free(ctx); + + return false; +} + +Future loadLibrwkv() async { + // Load the dynamic library according to the platform + String libFileName; + if (Platform.isMacOS) { + libFileName = 'librwkv.dylib'; + // } else if (Platform.isAndroid) { + // libFileName = 'librwkv.dylib'; + } else { + throw UnsupportedError('This platform is not supported yet.'); + } + + // Get the path of the dynamic library + String libPath = p.join('assets', 'rwkv', libFileName); + final data = await rootBundle.load(libPath); + + // Create a temporary file and write the data to it + final tempFile = File("${Directory.systemTemp.path}/$libFileName"); + await tempFile.writeAsBytes(data.buffer.asUint8List()); + + // Load the dynamic library from the temporary file + return DynamicLibrary.open(tempFile.path); +} diff --git a/shady_ai_flutter/lib/home/presentation/pages/home_page.dart b/shady_ai_flutter/lib/home/presentation/pages/home_page.dart new file mode 100644 index 0000000..9868306 --- /dev/null +++ b/shady_ai_flutter/lib/home/presentation/pages/home_page.dart @@ -0,0 +1,136 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_adaptive_scaffold/flutter_adaptive_scaffold.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:shady_ai/main.dart'; + +import '../widgets/home_grid_widget.dart'; + +class HomePage extends HookConsumerWidget { + const HomePage({Key? key}) : super(key: key); + + static const String routeName = '/'; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final selectedTab = useState(0); + // Define the children to display within the body at different breakpoints. + final List children = [ + HomeGridWidget( + text: 'Chat', + onPressed: () {}, + ), + const HomeGridWidget( + text: 'Translate', + ), + const HomeGridWidget( + text: 'Transcribe', + ), + const HomeGridWidget( + text: 'Music', + ), + const HomeGridWidget( + text: 'Grammar', + ), + const HomeGridWidget( + text: 'Summarize', + ), + const HomeGridWidget( + text: 'Code', + ), + ]; + return AdaptiveScaffold( + // An option to override the default breakpoints used for small, medium, + // and large. + smallBreakpoint: const WidthPlatformBreakpoint(end: 700), + mediumBreakpoint: const WidthPlatformBreakpoint(begin: 700, end: 1000), + largeBreakpoint: const WidthPlatformBreakpoint(begin: 1000), + useDrawer: false, + selectedIndex: selectedTab.value, + leadingExtendedNavRail: const SizedBox( + height: 8, + ), + + leadingUnextendedNavRail: const SizedBox( + height: 8, + ), + + onSelectedIndexChange: (int index) { + // Clear any snackbars for that instant feedback feel. + rootScaffoldMessengerKey.currentState?.clearSnackBars(); + + // Only allow the first tab to be selected. + if (index > 0) { + rootScaffoldMessengerKey.currentState?.showSnackBar( + const SnackBar( + content: Text('This page is not yet available.'), + ), + ); + return; + } + // Otherwise, update the selected tab. + selectedTab.value = index; + }, + destinations: const [ + NavigationDestination( + icon: Icon(Icons.rocket_launch_outlined), + selectedIcon: Icon(Icons.rocket_launch), + label: 'AI Suite', + ), + NavigationDestination( + icon: Icon(Icons.web), + selectedIcon: Icon(Icons.chat), + label: 'Feedback', + ), + NavigationDestination( + icon: Icon(Icons.settings_outlined), + selectedIcon: Icon(Icons.settings), + label: 'Settings', + ), + ], + body: (_) => Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Flexible( + flex: 1, + fit: FlexFit.tight, + child: Text( + 'Hey, Shady!', + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.displayLarge, + ), + ), + Flexible( + flex: 1, + child: Text( + "Let's get you started.", + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.displaySmall, + ), + ), + Expanded( + flex: 4, + child: GridView.count( + crossAxisCount: 2, + childAspectRatio: 3 / 2, + padding: const EdgeInsets.all(16), + mainAxisSpacing: 8, + crossAxisSpacing: 8, + children: children, + )) + ], + ), + smallBody: (_) => ListView.builder( + itemCount: children.length, + itemBuilder: (_, int idx) => children[idx], + ), + // Define a default secondaryBody. + secondaryBody: (_) => + Image.asset('assets/images/dad_cutout.png', fit: BoxFit.cover), + // Override the default secondaryBody during the smallBreakpoint to be + // empty. Must use AdaptiveScaffold.emptyBuilder to ensure it is properly + // overridden. + smallSecondaryBody: AdaptiveScaffold.emptyBuilder, + ); + } +} diff --git a/shady_ai_flutter/lib/home/presentation/widgets/home_grid_widget.dart b/shady_ai_flutter/lib/home/presentation/widgets/home_grid_widget.dart new file mode 100644 index 0000000..2212e94 --- /dev/null +++ b/shady_ai_flutter/lib/home/presentation/widgets/home_grid_widget.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; + +class HomeGridWidget extends StatelessWidget { + const HomeGridWidget({ + super.key, + required this.text, + this.onPressed, + }); + final String text; + final VoidCallback? onPressed; + + @override + Widget build(BuildContext context) { + return Material( + color: onPressed != null + ? Theme.of(context).colorScheme.surfaceVariant + : Theme.of(context).disabledColor, + type: MaterialType.card, + textStyle: Theme.of(context).textTheme.titleLarge, + child: InkWell( + onTap: onPressed != null + ? () { + ScaffoldMessenger.of(context).clearSnackBars(); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Sorry, this feature is not yet available.'), + ), + ); + } + : null, + child: Center( + child: FittedBox( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + text, + maxLines: 1, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.titleLarge, + ), + ), + ), + ), + ), + ); + } +} diff --git a/shady_ai_flutter/lib/main.dart b/shady_ai_flutter/lib/main.dart index 5e4dbc0..0fb3bd6 100644 --- a/shady_ai_flutter/lib/main.dart +++ b/shady_ai_flutter/lib/main.dart @@ -1,308 +1,49 @@ -import 'dart:async'; -import 'dart:ffi' as ffi; -import 'dart:ffi'; -import 'dart:io'; - -import 'package:ffi/ffi.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart' show rootBundle; -import 'package:path/path.dart' as p; -import 'package:shady_ai/generated/rwkv/rwkv_bindings.g.dart'; -import 'package:url_launcher/url_launcher.dart'; - -Future loadModelFile() async { - // TODO: For the time being, please add your model file to the assets/rwkv folder. - final String fileNameOfYourModel = 'daddy.bin'; - - // Get the path of the model file - String modelFilePath = p.join('assets', 'rwkv', fileNameOfYourModel); - final data = await rootBundle.load(modelFilePath); - - // Create a temporary file and write the data to it - final tempFile = File("${Directory.systemTemp.path}/$fileNameOfYourModel"); - - await tempFile.writeAsBytes(data.buffer.asUint8List()); - - // Return the path of the temporary model file - return tempFile.path; -} - -void main() { - runApp(const Main()); -} - -Future loadLibrwkv() async { - // Load the dynamic library according to the platform - String libFileName; - if (Platform.isMacOS) { - libFileName = 'librwkv.dylib'; - // } else if (Platform.isAndroid) { - // libFileName = 'librwkv.dylib'; - } else { - throw UnsupportedError('This platform is not supported yet.'); - } - - // Get the path of the dynamic library - String libPath = p.join('assets', 'rwkv', libFileName); - final data = await rootBundle.load(libPath); - - // Create a temporary file and write the data to it - final tempFile = File("${Directory.systemTemp.path}/$libFileName"); - await tempFile.writeAsBytes(data.buffer.asUint8List()); - - // Load the dynamic library from the temporary file - return ffi.DynamicLibrary.open(tempFile.path); -} - -class Main extends StatelessWidget { - const Main({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'ShadyAI', - debugShowCheckedModeBanner: false, - theme: ThemeData.light( - useMaterial3: true, - ), - darkTheme: ThemeData.dark( - useMaterial3: true, - ), - home: const MyHomePage(title: 'ShadyAI'), - ); - } +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:shady_ai/routing/router.dart'; +import 'package:shady_ai/themes/m3_dark_theme.dart'; +import 'package:shady_ai/themes/m3_light_theme.dart'; +import 'package:window_manager/window_manager.dart'; + +// Define a global key for the ScaffoldMessenger. Can be used to show snackbars. +final rootScaffoldMessengerKey = GlobalKey(); + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + await windowManager.ensureInitialized(); + WindowOptions windowOptions = const WindowOptions( + size: Size(1228, 944), + center: true, + backgroundColor: Colors.transparent, + skipTaskbar: false, + title: 'ShadyAI', + titleBarStyle: TitleBarStyle.hidden, + minimumSize: Size(944, 660), + ); + windowManager.waitUntilReadyToShow(windowOptions, () async { + await windowManager.show(); + await windowManager.focus(); + }); + runApp( + const ProviderScope( + child: Main(), + ), + ); } -class MyHomePage extends StatefulWidget { - const MyHomePage({Key? key, required this.title}) : super(key: key); - - final String title; +class Main extends HookConsumerWidget { + const Main({super.key}); @override - MyHomePageState createState() => MyHomePageState(); -} - -class MyHomePageState extends State { - // Load the model and evaluate it - // Returns true if the evaluation was successful - Future _loadModel() async { - RWKV? rwkv; - Pointer? ctx; - try { - rwkv = RWKV(await loadLibrwkv()); - final modelFilePath = await loadModelFile(); - final modelFilePathCStr = modelFilePath.toNativeUtf8().cast(); - ctx = rwkv.rwkv_init_from_file(modelFilePathCStr, 1); - - final state_buffer = rwkv.rwkv_get_state_buffer_element_count(ctx); - final Pointer state_buffer_ptr = - malloc.allocate(state_buffer); - - final logits_buffer = rwkv.rwkv_get_logits_buffer_element_count(ctx); - final Pointer logits_buffer_ptr = - malloc.allocate(logits_buffer); - - // TODO(BrutalCoding): Get the token from the user (e.g. via a text field or calculate it from the input) - final token = 103; - - final result = rwkv.rwkv_eval( - ctx, - token, - state_buffer_ptr, - state_buffer_ptr, - logits_buffer_ptr, - ); - - return result; - } catch (e) { - // Show an error message with ScaffoldMessenger - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Error: $e'), - ), - ); - } - if (rwkv == null || ctx == null) return false; - rwkv.rwkv_free(ctx); + Widget build(BuildContext context, WidgetRef ref) { + final router = ref.watch(routerProvider); - return false; - } - - Future? evalResult; - - @override - void initState() { - evalResult = _loadModel(); - super.initState(); - } - - @override - Widget build(BuildContext context) { - final textTheme = Theme.of(context).textTheme; - - return Scaffold( - appBar: AppBar( - title: Text(widget.title), - actions: [ - IconButton( - icon: const Icon(Icons.info_outline), - onPressed: () { - // Display a dialog with information about the app. - showDialog( - context: context, - builder: (context) { - return AlertDialog( - title: const Text('About ShadyAI'), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - SelectableText( - 'ShadyAI is a project by BrutalCoding.\n' - 'Its currently in a very early stage of development.\n\n' - "The app is currently only able to load in a model file and evaluate it. This does proof that the model file can be loaded and evaluated, and that's it.\n\n" - 'Coming Soon: Chat UI, drag-and-drop your AI models & built-in models.\n' - 'Coming Later: Adding more models, adding more features, adding more platforms.\n\n' - 'Please check out the GitHub repository for more information.', - ), - SizedBox(height: 8), - ListTile( - leading: Icon(Icons.link), - title: Text( - 'ShadyAI on GitHub', - ), - onTap: () { - launchUrl( - Uri.parse( - 'https://github.com/BrutalCoding/ShadyAI', - ), - ); - }, - ), - ListTile( - leading: Icon(Icons.link), - title: Text( - 'Follow me on Twitter', - ), - onTap: () { - launchUrl( - Uri.parse( - 'https://twitter.com/BrutalCoding', - ), - ); - }, - ), - ListTile( - leading: Icon(Icons.link), - title: Text( - 'Follow me on LinkedIn', - ), - onTap: () { - launchUrl( - Uri.parse( - 'https://linkedin.com/in/breedeveld/', - ), - ); - }, - ), - ], - ), - ); - }, - ); - }, - ), - ], - ), - body: SafeArea( - child: Padding( - padding: const EdgeInsets.all(32.0), - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Image.asset( - 'assets/dad_the_benchmark.png', - width: 200, - ), - Padding( - padding: const EdgeInsets.all(8.0), - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - 'AI Evaluation Result...', - style: textTheme.headlineMedium, - textAlign: TextAlign.center, - ), - ), - Text( - 'The app may freeze several times. This is normal.', - ), - SizedBox( - height: 32, - ), - FutureBuilder( - future: evalResult, - builder: (context, snapshot) { - if (snapshot.hasData) { - return snapshot.data == true - ? const Text( - 'The AI model is working correctly.', - style: TextStyle(color: Colors.green), - ) - : const Text( - 'The AI model is not working correctly.', - style: TextStyle(color: Colors.red), - ); - } else if (snapshot.hasError) { - return const Text( - 'An error occurred while evaluating the AI model.', - style: TextStyle( - color: Colors.red, - ), - ); - } else { - return const CircularProgressIndicator(); - } - }, - ), - ], - ), - ), - ), - ConstrainedBox( - constraints: const BoxConstraints( - maxWidth: 500, - ), - child: Column( - children: [ - Padding( - padding: const EdgeInsets.all(32.0), - child: Text( - "Dev instructions:\nAdd your model in 'shady.ai/shady_ai_flutter/assets/rwkv' and name it 'daddy.bin' 🚧", - style: textTheme.bodyLarge, - textAlign: TextAlign.center, - ), - ), - ], - ), - ), - Padding( - padding: const EdgeInsets.all(16.0), - child: ElevatedButton( - onPressed: () => _loadModel(), - child: const Text('Retry'), - ), - ), - ], - ), - ), - ), - ), + return MaterialApp.router( + scaffoldMessengerKey: rootScaffoldMessengerKey, + theme: m3LightTheme, + darkTheme: m3DarkTheme, + debugShowCheckedModeBanner: false, + routerConfig: router, ); } } diff --git a/shady_ai_flutter/lib/routing/router.dart b/shady_ai_flutter/lib/routing/router.dart new file mode 100644 index 0000000..7ef00ad --- /dev/null +++ b/shady_ai_flutter/lib/routing/router.dart @@ -0,0 +1,17 @@ +import 'package:go_router/go_router.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +import '../home/presentation/pages/home_page.dart'; + +// GoRouter configuration +final _router = GoRouter( + routes: [ + GoRoute( + path: '/', + builder: (_, __) => const HomePage(), + ), + ], +); + +// Create a provider to hold the router with Riverpod +final routerProvider = Provider((_) => _router); diff --git a/shady_ai_flutter/lib/themes/color_schemes.dart b/shady_ai_flutter/lib/themes/color_schemes.dart new file mode 100644 index 0000000..2a0bb1b --- /dev/null +++ b/shady_ai_flutter/lib/themes/color_schemes.dart @@ -0,0 +1,107 @@ +import 'package:flutter/material.dart'; + +const lightColorScheme = ColorScheme( + brightness: Brightness.light, + primary: Color(0xFF626200), + onPrimary: Color(0xFFFFFFFF), + primaryContainer: Color(0xFFEAEA00), + onPrimaryContainer: Color(0xFF1D1D00), + // primaryFixed: Color(0xFFEAEA00), + // onPrimaryFixed: Color(0xFF1D1D00), + // primaryFixedDim: Color(0xFFCDCD00), + // onPrimaryFixedVariant: Color(0xFF494900), + secondary: Color(0xFF963792), + onSecondary: Color(0xFFFFFFFF), + secondaryContainer: Color(0xFFFFD7F5), + onSecondaryContainer: Color(0xFF380038), + // secondaryFixed: Color(0xFFFFD7F5), + // onSecondaryFixed: Color(0xFF380038), + // secondaryFixedDim: Color(0xFFFFAAF3), + // onSecondaryFixedVariant: Color(0xFF7A1B78), + tertiary: Color(0xFF3D6657), + onTertiary: Color(0xFFFFFFFF), + tertiaryContainer: Color(0xFFBFECD8), + onTertiaryContainer: Color(0xFF002117), + // tertiaryFixed: Color(0xFFBFECD8), + // onTertiaryFixed: Color(0xFF002117), + // tertiaryFixedDim: Color(0xFFA4D0BD), + // onTertiaryFixedVariant: Color(0xFF254E40), + error: Color(0xFFBA1A1A), + errorContainer: Color(0xFFFFDAD6), + onError: Color(0xFFFFFFFF), + onErrorContainer: Color(0xFF410002), + background: Color(0xFFFFFBFF), + onBackground: Color(0xFF1C1C17), + outline: Color(0xFF797869), + onInverseSurface: Color(0xFFF4F0E8), + inverseSurface: Color(0xFF31312B), + inversePrimary: Color(0xFFCDCD00), + shadow: Color(0xFF000000), + surfaceTint: Color(0xFF626200), + outlineVariant: Color(0xFFCAC7B6), + scrim: Color(0xFF000000), + surface: Color(0xFFFDF9F0), + onSurface: Color(0xFF1C1C17), + surfaceVariant: Color(0xFFE6E3D1), + onSurfaceVariant: Color(0xFF48473A), + // surfaceContainerHighest: Color(0xFFE6E2D9), + // surfaceContainerHigh: Color(0xFFEBE8DF), + // surfaceContainer: Color(0xFFF1EEE5), + // surfaceContainerLow: Color(0xFFF7F3EA), + // surfaceContainerLowest: Color(0xFFFFFFFF), + // surfaceDim: Color(0xFFDDDAD1), + // surfaceBright: Color(0xFFFDF9F0), +); + +const darkColorScheme = ColorScheme( + brightness: Brightness.dark, + primary: Color(0xFFCDCD00), + onPrimary: Color(0xFF323200), + primaryContainer: Color(0xFF494900), + onPrimaryContainer: Color(0xFFEAEA00), + // primaryFixed: Color(0xFFEAEA00), + // onPrimaryFixed: Color(0xFF1D1D00), + // primaryFixedDim: Color(0xFFCDCD00), + // onPrimaryFixedVariant: Color(0xFF494900), + secondary: Color(0xFFFFAAF3), + onSecondary: Color(0xFF5B005B), + secondaryContainer: Color(0xFF7A1B78), + onSecondaryContainer: Color(0xFFFFD7F5), + // secondaryFixed: Color(0xFFFFD7F5), + // onSecondaryFixed: Color(0xFF380038), + // secondaryFixedDim: Color(0xFFFFAAF3), + // onSecondaryFixedVariant: Color(0xFF7A1B78), + tertiary: Color(0xFFA4D0BD), + onTertiary: Color(0xFF0B372A), + tertiaryContainer: Color(0xFF254E40), + onTertiaryContainer: Color(0xFFBFECD8), + // tertiaryFixed: Color(0xFFBFECD8), + // onTertiaryFixed: Color(0xFF002117), + // tertiaryFixedDim: Color(0xFFA4D0BD), + // onTertiaryFixedVariant: Color(0xFF254E40), + error: Color(0xFFFFB4AB), + errorContainer: Color(0xFF93000A), + onError: Color(0xFF690005), + onErrorContainer: Color(0xFFFFDAD6), + background: Color(0xFF1C1C17), + onBackground: Color(0xFFE6E2D9), + outline: Color(0xFF939182), + onInverseSurface: Color(0xFF1C1C17), + inverseSurface: Color(0xFFE6E2D9), + inversePrimary: Color(0xFF626200), + shadow: Color(0xFF000000), + surfaceTint: Color(0xFFCDCD00), + outlineVariant: Color(0xFF48473A), + scrim: Color(0xFF000000), + surface: Color(0xFF14140F), + onSurface: Color(0xFFC9C6BE), + surfaceVariant: Color(0xFF48473A), + onSurfaceVariant: Color(0xFFCAC7B6), + // surfaceContainerHighest: Color(0xFF36352F), + // surfaceContainerHigh: Color(0xFF2B2A25), + // surfaceContainer: Color(0xFF20201A), + // surfaceContainerLow: Color(0xFF1C1C17), + // surfaceContainerLowest: Color(0xFF0F0E0A), + // surfaceDim: Color(0xFF14140F), + // surfaceBright: Color(0xFF3A3933), +); diff --git a/shady_ai_flutter/lib/themes/m3_dark_theme.dart b/shady_ai_flutter/lib/themes/m3_dark_theme.dart new file mode 100644 index 0000000..9608943 --- /dev/null +++ b/shady_ai_flutter/lib/themes/m3_dark_theme.dart @@ -0,0 +1,11 @@ +import 'package:flutter/material.dart'; +import 'package:shady_ai/themes/color_schemes.dart'; + +ThemeData get m3DarkTheme => ThemeData( + useMaterial3: true, + colorScheme: darkColorScheme, + splashFactory: NoSplash.splashFactory, + bottomNavigationBarTheme: const BottomNavigationBarThemeData( + type: BottomNavigationBarType.fixed, + ), + ); diff --git a/shady_ai_flutter/lib/themes/m3_light_theme.dart b/shady_ai_flutter/lib/themes/m3_light_theme.dart new file mode 100644 index 0000000..eca4df9 --- /dev/null +++ b/shady_ai_flutter/lib/themes/m3_light_theme.dart @@ -0,0 +1,12 @@ +import 'package:flutter/material.dart'; + +import 'color_schemes.dart'; + +ThemeData get m3LightTheme => ThemeData.light().copyWith( + useMaterial3: true, + colorScheme: lightColorScheme, + splashFactory: NoSplash.splashFactory, + bottomNavigationBarTheme: const BottomNavigationBarThemeData( + type: BottomNavigationBarType.fixed, + ), + ); diff --git a/shady_ai_flutter/macos/Runner/DebugProfile.entitlements b/shady_ai_flutter/macos/Runner/DebugProfile.entitlements index 78c36cf..4fc43ef 100644 --- a/shady_ai_flutter/macos/Runner/DebugProfile.entitlements +++ b/shady_ai_flutter/macos/Runner/DebugProfile.entitlements @@ -10,5 +10,7 @@ com.apple.security.network.client + com.apple.security.files.user-selected.read-write + diff --git a/shady_ai_flutter/macos/Runner/Release.entitlements b/shady_ai_flutter/macos/Runner/Release.entitlements index 08ba3a3..ed179b3 100644 --- a/shady_ai_flutter/macos/Runner/Release.entitlements +++ b/shady_ai_flutter/macos/Runner/Release.entitlements @@ -6,5 +6,7 @@ com.apple.security.network.client + com.apple.security.files.user-selected.read-only + diff --git a/shady_ai_flutter/mason-lock.json b/shady_ai_flutter/mason-lock.json new file mode 100644 index 0000000..0df6d74 --- /dev/null +++ b/shady_ai_flutter/mason-lock.json @@ -0,0 +1 @@ +{"bricks":{"feature_brick":"0.6.2"}} \ No newline at end of file diff --git a/shady_ai_flutter/mason.yaml b/shady_ai_flutter/mason.yaml new file mode 100644 index 0000000..1f64e07 --- /dev/null +++ b/shady_ai_flutter/mason.yaml @@ -0,0 +1,2 @@ +bricks: + feature_brick: ^0.6.2 \ No newline at end of file diff --git a/shady_ai_flutter/pubspec.yaml b/shady_ai_flutter/pubspec.yaml index 2fada1a..0d19a7e 100644 --- a/shady_ai_flutter/pubspec.yaml +++ b/shady_ai_flutter/pubspec.yaml @@ -29,11 +29,19 @@ dependencies: path: ../shady_ai_client cupertino_icons: ^1.0.2 ffi: ^2.0.1 - hooks_riverpod: ^2.3.4 + hooks_riverpod: ^2.3.6 riverpod_generator: ^2.1.6 riverpod_lint: ^1.2.0 go_router: ^6.5.5 url_launcher: ^6.1.10 + file_selector: ^0.9.2+4 + desktop_drop: ^0.4.1 + path_provider: ^2.0.14 + path: ^1.8.2 + flutter_adaptive_scaffold: ^0.1.3 + riverpod_annotation: ^2.1.1 + flutter_hooks: ^0.18.6 + window_manager: ^0.3.2 # # Experimental. I was able to compile "flutter build web --wasm" but only without serverpod_flutter. # serverpod_flutter relies on a connectivity package, which uses dart:html. @@ -47,12 +55,15 @@ dependencies: # path: flutter_wasm dev_dependencies: + build_runner: ^2.3.3 + custom_lint: ^0.3.4 dart_code_metrics: ^5.7.2 ffigen: ^7.2.10 flutter_launcher_icons: ^0.13.0 flutter_lints: ^2.0.1 flutter_test: sdk: flutter + go_router_builder: ^1.2.2 ffigen: name: RWKV @@ -92,5 +103,5 @@ flutter: # the material Icons class. uses-material-design: true assets: - - assets/dad_the_benchmark.png + - assets/images/ - assets/rwkv/ diff --git a/shady_ai_flutter/test/chat/presentation/chat_page_test.dart b/shady_ai_flutter/test/chat/presentation/chat_page_test.dart new file mode 100644 index 0000000..31783de --- /dev/null +++ b/shady_ai_flutter/test/chat/presentation/chat_page_test.dart @@ -0,0 +1,21 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:shady_ai/chat/presentation/chat_page.dart'; + +void main() { + group('ChatPage', () { + testWidgets('renders ChatPage', (tester) async { + await tester.pumpWidget( + ProviderScope( + child: MaterialApp( + home: ChatPage(), + ), + ), + ); + expect(find.byKey(const Key('chat_page_scaffold')), findsOneWidget); + }); + }); +} diff --git a/shady_ai_flutter/test/widget_test.dart b/shady_ai_flutter/test/widget_test.dart index 92469ed..d924d17 100644 --- a/shady_ai_flutter/test/widget_test.dart +++ b/shady_ai_flutter/test/widget_test.dart @@ -2,7 +2,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:shady_ai/main.dart'; void main() { - testWidgets('Test main instance', (WidgetTester tester) async { + testWidgets('Test home page instance', (WidgetTester tester) async { // Build our app and trigger a frame. await tester.pumpWidget(const Main()); });