diff --git a/lib/components/news.dart b/lib/components/news.dart index 6696efb..1940785 100644 --- a/lib/components/news.dart +++ b/lib/components/news.dart @@ -111,7 +111,7 @@ class AIFetchingTask { } class NewsController extends GetxController { - HashMap _pending_tasks = HashMap(); + HashMap _pending_tasks = HashMap(); RxList _$cached = [].obs; RxList _$record = [].obs; @@ -202,7 +202,7 @@ class NewsController extends GetxController { Future recommendNews(List news, {UserProfile? profile}) async { - final id = Uuid(); + final id = Uuid().v4(); _pending_tasks[id] = AIFetchingTask(source: news); try { final currentPromoted = diff --git a/lib/components/setting.dart b/lib/components/setting.dart index f5a2600..cee9cbc 100644 --- a/lib/components/setting.dart +++ b/lib/components/setting.dart @@ -1,8 +1,13 @@ +import 'dart:developer'; + +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:moyubie/controller/chat_room.dart'; import 'package:moyubie/controller/settings.dart'; import 'package:get/get.dart'; import 'package:moyubie/repository/chat_room.dart'; +import 'package:moyubie/repository/tags.dart'; +import 'package:uuid/uuid.dart'; class SettingPage extends StatefulWidget { const SettingPage({super.key}); @@ -361,6 +366,51 @@ class _SettingPageState extends State { child: const Text("Clear remote messages")), ], ), + // DEBUGGER + if (kDebugMode) ...[ + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + const Text("Debugger"), + Tooltip( + message: "You can see me?", + child: IconButton( + iconSize: 10.0, + splashRadius: 10, + color: Theme.of(context).colorScheme.primary, + onPressed: () {}, + icon: const Icon(Icons.question_mark), + ), + ), + ], + ), + divider, + sizedBoxSpace, + Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ElevatedButton( + onPressed: () { + final repo = Get.find(); + repo + .addNewTags(List.generate(10, (index) => Uuid().v4())) + .then((value) => log("DONE?", name: "moyubie::tags")) + .catchError((err) => log("ERROR! [$err]", name: "moyubie::tags")); + }, + child: const Text("Add some random tags for you!")), + ElevatedButton( + onPressed: () { + final repo = Get.find(); + repo + .fetchMostPopularTags(5) + .then((value) => log("DONE? [$value]", name: "moyubie::tags")) + .catchError((err) => log("ERROR! [$err]", name: "moyubie::tags")); + }, + child: const Text("Fetch tags of you!")), + ], + ) + ] ], ); }), diff --git a/lib/main.dart b/lib/main.dart index 3db3994..efbda18 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -8,6 +8,7 @@ import 'package:moyubie/controller/settings.dart'; import 'package:moyubie/components/setting.dart'; import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; +import 'package:moyubie/repository/tags.dart'; import 'package:sqflite_common_ffi_web/sqflite_ffi_web.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import 'package:moyubie/configs/translations.dart'; @@ -88,6 +89,7 @@ class MyApp extends StatelessWidget { Get.put(MessageController()); Get.put(PromptController()); Get.put(ChatRoomController()); + Get.put(TagsRepository.byConfig(settingsCtl)); Get.put(NewsController(settingsCtl.openAiKey, settingsCtl.gptModel)); final newsWinKey = GlobalKey(); return MaterialApp( diff --git a/lib/repository/tags.dart b/lib/repository/tags.dart index 04f1b04..f05fb75 100644 --- a/lib/repository/tags.dart +++ b/lib/repository/tags.dart @@ -1,39 +1,95 @@ +import 'dart:developer'; + +import 'package:get/get.dart'; +import 'package:get/get_rx/get_rx.dart'; +import 'package:moyubie/controller/settings.dart'; +import 'package:moyubie/utils/tidb.dart'; import 'package:mysql_client/mysql_client.dart'; +import 'package:stream_transform/stream_transform.dart'; + +class _CachedConnection { + MySQLConnection? _last; + + _CachedConnection(Stream stream) { + stream.listen((event) { + if (_last != null && _last != event) { + log("Update connection. [old=$_last, new=$event]", name: "moyubie::_CachedConnection"); + _last!.close(); + } + _last = event; + }, + onError: (err) => + log("ERROR [$err]", name: "moyubie::_CachedConnection"), + onDone: () => log("APP CLOSED", name: "moyubie::_CachedConnection"), + cancelOnError: false); + } -import 'chat_room.dart'; + MySQLConnection? get value => _last; +} class TagsRepository { - static Future getRemoteDb(TiDBConnection conn, - {bool forceInit = false}) async { - bool shouldInit = - conn.connection == null || !conn.connection!.connected || forceInit; - if (conn.host.isEmpty || - conn.port == 0 || - conn.userName.isEmpty || - conn.password.isEmpty) { - shouldInit = false; - } + factory TagsRepository.byConfig(SettingsController ctl, + {bool forceInit = false}) { + final stream = ctl.serverlessCmd.stream.switchMap((p0) { + final (host, port, user, password) = parseTiDBConnectionText(p0); + if (host.isEmpty || port == 0 || user.isEmpty) { + return Stream.error(Exception("Invalid DB")); + } + return Stream.fromFuture(() async { + final conn = await MySQLConnection.createConnection( + host: host, port: port, userName: user, password: password); + await conn.connect(); + await prepareTables(conn); + return conn; + }()); + }); + return TagsRepository(_CachedConnection(stream)); + } + + final _CachedConnection _conn; - if (shouldInit) { - // Make sure the old connection has been close - conn.connection?.close(); + TagsRepository(this._conn); - var dbConn = await MySQLConnection.createConnection( - host: conn.host, - port: conn.port, - userName: conn.userName, - password: conn.password); - conn.connection = dbConn; + static const _tagName = "name"; + static const _tagAddedAt = "added_at"; + static const _table = "tags"; + static const _db = "moyubie"; - await dbConn.connect(); + static Future prepareTables(MySQLConnection conn) async { + await conn.execute("CREATE DATABASE IF NOT EXISTS $_db"); + await conn.execute("CREATE TABLE IF NOT EXISTS `$_db`.$_table(" + "$_tagName TEXT," + "$_tagAddedAt DATETIME," + "INDEX sand_of_time(added_at)" + ");"); + } - dbConn.onClose(() { - // I haven't check the client carefully. - // Is it enough to handle connection broken or someting bad? - conn.connection = null; - }); + Future addNewTags(List tags) async { + final now = DateTime.now(); + if (_conn.value == null) { + throw Exception("The connection isn't ready for now..."); } + final insert = await _conn.value!.prepare( + "INSERT INTO $_db.$_table($_tagName, $_tagAddedAt) VALUES (?, ?);"); + await Future.wait(tags.map((e) => insert.execute([e, now])), + eagerError: true); + } - return conn.connection; + Future> fetchMostPopularTags(int limit) async { + if (_conn.value == null) { + throw Exception("The connection isn't ready for now..."); + } + final res = await _conn.value!.execute( + "SELECT t1.NAME AS $_tagName FROM " + "(SELECT COUNT($_tagName) AS CNT, $_tagName AS NAME FROM $_db.$_table GROUP BY $_tagName) AS t1" + " ORDER BY t1.CNT LIMIT :limit; ", + {"limit": limit}); + final output = []; + for (final rs in res) { + for (final row in rs.rows) { + output.add(row.colByName(_tagName) as String); + } + } + return output; } } diff --git a/lib/utils/ai_recommend.dart b/lib/utils/ai_recommend.dart index f295e45..4c7a359 100644 --- a/lib/utils/ai_recommend.dart +++ b/lib/utils/ai_recommend.dart @@ -216,6 +216,7 @@ class NewsPromoter extends WithOpenAI { ], // Make the output more predictable. topP: 0.08, + presencePenalty: 1, functionCall: FunctionCall.forFunction(_fn_recommend_news.name), functions: [_fn_get_news, _fn_get_user_info, _fn_recommend_news]); final args = res.choices[0].message.functionCall?.arguments;