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 👇
-
+
## 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
+
+
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());
});