Critical chat fix, added voice mode
This commit is contained in:
parent
93818cfbad
commit
baf654be40
|
@ -50,6 +50,20 @@
|
|||
<data android:scheme="https"/>
|
||||
</intent>
|
||||
<!-- check if url is valid end -->
|
||||
<!-- voice mode -->
|
||||
<intent>
|
||||
<action android:name="android.speech.RecognitionService" />
|
||||
</intent>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.TTS_SERVICE" />
|
||||
</intent>
|
||||
<!-- voice mode end-->
|
||||
</queries>
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<!-- voice mode -->
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
|
||||
<uses-permission android:name="android.permission.BLUETOOTH"/>
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
|
||||
<!-- voice mode end -->
|
||||
</manifest>
|
|
@ -20,7 +20,7 @@ pluginManagement {
|
|||
plugins {
|
||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||
id "com.android.application" version "7.3.0" apply false
|
||||
id "org.jetbrains.kotlin.android" version "1.7.10" apply false
|
||||
id "org.jetbrains.kotlin.android" version "2.0.0" apply false
|
||||
}
|
||||
|
||||
include ":app"
|
||||
|
|
|
@ -145,6 +145,11 @@
|
|||
"description": "Title of the interface settings section",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsTitleVoice": "Voice",
|
||||
"@settingsTitleVoice": {
|
||||
"description": "Title of the voice settings section. Do not translate if not required!",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsTitleExport": "Exportieren",
|
||||
"@settingsTitleExport": {
|
||||
"description": "Title of the export settings section",
|
||||
|
@ -160,6 +165,36 @@
|
|||
"description": "Text displayed when settings are saved automatically",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsExperimentalAlpha": "alpha",
|
||||
"@settingsExperimentalAlpha": {
|
||||
"description": "Text displayed when a feature is in alpha",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsExperimentalAlphaDescription": "Diese Funktion befindet sich im Alpha-Status und funktioniert möglicherweise nicht wie beabsichtigt oder erwartet.\nKritische Probleme und/oder dauerhafte kritische Schäden am Gerät und/oder den verwendeten Diensten können nicht ausgeschlossen werden.\nBenutzung auf eigene Gefahr. Keine Haftung seitens des App-Autors.",
|
||||
"@settingsExperimentalAlphaDescription": {
|
||||
"description": "Description of the alpha feature",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsExperimentalAlphaFeature": "Alpha-Funktion, halte, um mehr zu erfahren",
|
||||
"@settingsExperimentalAlphaFeature": {
|
||||
"description": "Text displayed when a feature is in alpha",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsExperimentalBeta": "beta",
|
||||
"@settingsExperimentalBeta": {
|
||||
"description": "Text displayed when a feature is in beta",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsExperimentalBetaDescription": "Diese Funktion befindet sich im Beta-Test und funktioniert möglicherweise nicht wie beabsichtigt oder erwartet.\nWeniger schwerwiegende Probleme können auftreten oder auch nicht. Schäden sollten nicht kritisch sein.\nVerwendung auf eigene Gefahr.",
|
||||
"@settingsExperimentalBetaDescription": {
|
||||
"description": "Description of the beta feature",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsExperimentalBetaFeature": "Beta-Funktion, halte, um mehr zu erfahren",
|
||||
"@settingsExperimentalBetaFeature": {
|
||||
"description": "Text displayed when a feature is in beta",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsHost": "Host",
|
||||
"@settingsHost": {
|
||||
"description": "Text displayed as description for host input",
|
||||
|
@ -333,6 +368,41 @@
|
|||
"description": "Text displayed for cancel button, should be capitalized",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsVoicePermissionLoading": "Lade Sprachberechtigungen ...",
|
||||
"@settingsVoicePermissionLoading": {
|
||||
"description": "Text displayed while loading voice permissions",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsVoicePermissionNot": "Berechtigungen nicht erteilt",
|
||||
"@settingsVoicePermissionNot": {
|
||||
"description": "Text displayed when voice permissions are not granted",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsVoiceNotSupported": "Voice-Modus wird nicht unterstützt",
|
||||
"@settingsVoiceNotSupported": {
|
||||
"description": "Text displayed when voice mode is not supported",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsVoiceEnable": "Voice-Modus aktivieren",
|
||||
"@settingsVoiceEnable": {
|
||||
"description": "Text displayed as description for enable voice mode toggle",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsVoiceNoLanguage": "Keine Sprache ausgewählt",
|
||||
"@settingsVoiceNoLanguage": {
|
||||
"description": "Text displayed when no language is selected",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsVoiceLimitLanguage": "Auf gewählte Sprache beschränken",
|
||||
"@settingsVoiceLimitLanguage": {
|
||||
"description": "Text displayed as description for limit language toggle",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsVoicePunctuation": "KI Satzzeichen aktivieren",
|
||||
"@settingsVoicePunctuation": {
|
||||
"description": "Text displayed as description for enable AI punctuation toggle",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsExportChats": "Chats exportieren",
|
||||
"@settingsExportChats": {
|
||||
"description": "Text displayed as description for export chats button",
|
||||
|
@ -378,11 +448,6 @@
|
|||
"description": "Warning displayed for export and import options",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsCheckForUpdates": "Beim Öffnen nach Updates suchen",
|
||||
"@settingsCheckForUpdates": {
|
||||
"description": "Text displayed as description for check for updates toggle",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsUpdateCheck": "Nach Updates suchen",
|
||||
"@settingsUpdateCheck": {
|
||||
"description": "Text displayed as description for check for updates button",
|
||||
|
@ -444,6 +509,11 @@
|
|||
"description": "Text displayed for cancel button, should be capitalized",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsCheckForUpdates": "Beim Öffnen nach Updates suchen",
|
||||
"@settingsCheckForUpdates": {
|
||||
"description": "Text displayed as description for check for updates toggle",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsGithub": "GitHub",
|
||||
"@settingsGithub": {
|
||||
"description": "Text displayed as description for GitHub button",
|
||||
|
|
|
@ -145,6 +145,11 @@
|
|||
"description": "Title of the interface settings section",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsTitleVoice": "Voice",
|
||||
"@settingsTitleVoice": {
|
||||
"description": "Title of the voice settings section. Do not translate if not required!",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsTitleExport": "Export",
|
||||
"@settingsTitleExport": {
|
||||
"description": "Title of the export settings section",
|
||||
|
@ -160,6 +165,36 @@
|
|||
"description": "Text displayed when settings are saved automatically",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsExperimentalAlpha": "alpha",
|
||||
"@settingsExperimentalAlpha": {
|
||||
"description": "Text displayed when a feature is in alpha",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsExperimentalAlphaDescription": "This feature is in alpha and may not work as intended or expected.\nCritical issues and/or permanent critical damage to device and/or used services cannot be ruled out.\nUse at your own risk. No liability on the part of the app author.",
|
||||
"@settingsExperimentalAlphaDescription": {
|
||||
"description": "Description of the alpha feature",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsExperimentalAlphaFeature": "Alpha feature, hold to learn more",
|
||||
"@settingsExperimentalAlphaFeature": {
|
||||
"description": "Text displayed when a feature is in alpha",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsExperimentalBeta": "beta",
|
||||
"@settingsExperimentalBeta": {
|
||||
"description": "Text displayed when a feature is in beta",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsExperimentalBetaDescription": "This feature is in beta and may not work intended or expected.\nLess severe issues may or may not occur. Damage shouldn't be critical.\nUse at your own risk.",
|
||||
"@settingsExperimentalBetaDescription": {
|
||||
"description": "Description of the beta feature",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsExperimentalBetaFeature": "Beta feature, hold to learn more",
|
||||
"@settingsExperimentalBetaFeature": {
|
||||
"description": "Text displayed when a feature is in beta",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsHost": "Host",
|
||||
"@settingsHost": {
|
||||
"description": "Text displayed as description for host input",
|
||||
|
@ -333,6 +368,41 @@
|
|||
"description": "Text displayed for cancel button, should be capitalized",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsVoicePermissionLoading": "Loading voice permissions ...",
|
||||
"@settingsVoicePermissionLoading": {
|
||||
"description": "Text displayed while loading voice permissions",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsVoicePermissionNot": "Permissions not granted",
|
||||
"@settingsVoicePermissionNot": {
|
||||
"description": "Text displayed when voice permissions are not granted",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsVoiceNotSupported": "Voice mode not supported",
|
||||
"@settingsVoiceNotSupported": {
|
||||
"description": "Text displayed when voice mode is not supported",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsVoiceEnable": "Enable voice mode",
|
||||
"@settingsVoiceEnable": {
|
||||
"description": "Text displayed as description for enable voice mode toggle",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsVoiceNoLanguage": "No language selected",
|
||||
"@settingsVoiceNoLanguage": {
|
||||
"description": "Text displayed when no language is selected",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsVoiceLimitLanguage": "Limit to selected language",
|
||||
"@settingsVoiceLimitLanguage": {
|
||||
"description": "Text displayed as description for limit language toggle",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsVoicePunctuation": "Enable AI punctuation",
|
||||
"@settingsVoicePunctuation": {
|
||||
"description": "Text displayed as description for enable AI punctuation toggle",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsExportChats": "Export chats",
|
||||
"@settingsExportChats": {
|
||||
"description": "Text displayed as description for export chats button",
|
||||
|
|
353
lib/main.dart
353
lib/main.dart
|
@ -10,9 +10,11 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
|||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import 'screen_settings.dart';
|
||||
import 'screen_voice.dart';
|
||||
import 'screen_welcome.dart';
|
||||
import 'worker/setter.dart';
|
||||
import 'worker/haptic.dart';
|
||||
import 'worker/sender.dart';
|
||||
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
// ignore: depend_on_referenced_packages
|
||||
|
@ -22,13 +24,14 @@ import 'package:uuid/uuid.dart';
|
|||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:visibility_detector/visibility_detector.dart';
|
||||
// import 'package:http/http.dart' as http;
|
||||
import 'package:ollama_dart/ollama_dart.dart' as llama;
|
||||
import 'package:dartx/dartx.dart';
|
||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
// ignore: depend_on_referenced_packages
|
||||
import 'package:markdown/markdown.dart' as md;
|
||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||
import 'package:flutter_displaymode/flutter_displaymode.dart';
|
||||
import 'package:speech_to_text/speech_to_text.dart';
|
||||
import 'package:flutter_tts/flutter_tts.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
// client configuration
|
||||
|
||||
|
@ -66,6 +69,15 @@ final user = types.User(id: const Uuid().v4());
|
|||
final assistant = types.User(id: const Uuid().v4());
|
||||
|
||||
bool settingsOpen = false;
|
||||
bool logoVisible = true;
|
||||
bool menuVisible = false;
|
||||
bool sendable = false;
|
||||
|
||||
SpeechToText speech = SpeechToText();
|
||||
FlutterTts voice = FlutterTts();
|
||||
bool voiceSupported = false;
|
||||
|
||||
Function? setMainState;
|
||||
|
||||
void main() {
|
||||
runApp(const App());
|
||||
|
@ -101,6 +113,12 @@ class _AppState extends State<App> {
|
|||
try {
|
||||
await FlutterDisplayMode.setHighRefreshRate();
|
||||
} catch (_) {}
|
||||
|
||||
if ((await Permission.bluetoothConnect.isGranted) &&
|
||||
(await Permission.microphone.isGranted)) {
|
||||
voiceSupported = await speech.initialize();
|
||||
}
|
||||
|
||||
SharedPreferences.setPrefix("ollama.");
|
||||
SharedPreferences tmp = await SharedPreferences.getInstance();
|
||||
setState(() {
|
||||
|
@ -177,11 +195,7 @@ class MainApp extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _MainAppState extends State<MainApp> {
|
||||
bool logoVisible = true;
|
||||
bool menuVisible = false;
|
||||
|
||||
int tipId = Random().nextInt(5);
|
||||
bool sendable = false;
|
||||
|
||||
List<Widget> sidebar(BuildContext context, Function setState) {
|
||||
return List.from([
|
||||
|
@ -747,18 +761,14 @@ class _MainAppState extends State<MainApp> {
|
|||
]),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
HapticFeedback
|
||||
.selectionClick();
|
||||
onPressed: () {selectionHaptic();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(AppLocalizations.of(
|
||||
context)!
|
||||
.deleteDialogCancel)),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
HapticFeedback
|
||||
.selectionClick();
|
||||
onPressed: () {selectionHaptic();
|
||||
Navigator.of(context).pop();
|
||||
|
||||
for (var i = 0;
|
||||
|
@ -1083,259 +1093,8 @@ class _MainAppState extends State<MainApp> {
|
|||
child: const ImageIcon(
|
||||
AssetImage("assets/logo512.png"),
|
||||
size: 44)))),
|
||||
onSendPressed: (p0) async {
|
||||
selectionHaptic();
|
||||
setState(() {
|
||||
sendable = false;
|
||||
});
|
||||
|
||||
if (host == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text(
|
||||
AppLocalizations.of(context)!.noHostSelected),
|
||||
showCloseIcon: true));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!chatAllowed || model == null) {
|
||||
if (model == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text(AppLocalizations.of(context)!
|
||||
.noModelSelected),
|
||||
showCloseIcon: true));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
bool newChat = false;
|
||||
if (chatUuid == null) {
|
||||
newChat = true;
|
||||
chatUuid = const Uuid().v4();
|
||||
prefs!.setStringList(
|
||||
"chats",
|
||||
(prefs!.getStringList("chats") ?? []).append([
|
||||
jsonEncode({
|
||||
"title": AppLocalizations.of(context)!
|
||||
.newChatTitle,
|
||||
"uuid": chatUuid,
|
||||
"messages": []
|
||||
})
|
||||
]).toList());
|
||||
}
|
||||
|
||||
var system = prefs?.getString("system") ??
|
||||
"You are a helpful assistant";
|
||||
if (prefs!.getBool("noMarkdown") ?? false) {
|
||||
system +=
|
||||
" You must not use markdown or any other formatting language in any way!";
|
||||
}
|
||||
|
||||
List<llama.Message> history = [
|
||||
llama.Message(
|
||||
role: llama.MessageRole.system, content: system)
|
||||
];
|
||||
List<String> images = [];
|
||||
for (var i = 0; i < messages.length; i++) {
|
||||
if (jsonDecode(jsonEncode(messages[i]))["text"] !=
|
||||
null) {
|
||||
history.add(llama.Message(
|
||||
role: (messages[i].author.id == user.id)
|
||||
? llama.MessageRole.user
|
||||
: llama.MessageRole.system,
|
||||
content:
|
||||
jsonDecode(jsonEncode(messages[i]))["text"],
|
||||
images: (images.isNotEmpty) ? images : null));
|
||||
} else {
|
||||
var uri = jsonDecode(jsonEncode(messages[i]))["uri"]
|
||||
as String;
|
||||
String content = (uri
|
||||
.startsWith("data:image/png;base64,"))
|
||||
? uri.removePrefix("data:image/png;base64,")
|
||||
: base64.encode(await File(uri).readAsBytes());
|
||||
uri = uri.removePrefix("data:image/png;base64,");
|
||||
images.add(content);
|
||||
}
|
||||
}
|
||||
|
||||
history.add(llama.Message(
|
||||
role: llama.MessageRole.user,
|
||||
content: p0.text.trim(),
|
||||
images: (images.isNotEmpty) ? images : null));
|
||||
messages.insert(
|
||||
0,
|
||||
types.TextMessage(
|
||||
author: user,
|
||||
id: const Uuid().v4(),
|
||||
text: p0.text.trim()));
|
||||
|
||||
saveChat(chatUuid!, setState);
|
||||
|
||||
setState(() {});
|
||||
chatAllowed = false;
|
||||
|
||||
String newId = const Uuid().v4();
|
||||
llama.OllamaClient client = llama.OllamaClient(
|
||||
headers: (jsonDecode(
|
||||
prefs!.getString("hostHeaders") ?? "{}")
|
||||
as Map)
|
||||
.cast<String, String>(),
|
||||
baseUrl: "$host/api");
|
||||
|
||||
try {
|
||||
if ((prefs!.getString("requestType") ?? "stream") ==
|
||||
"stream") {
|
||||
final stream = client
|
||||
.generateChatCompletionStream(
|
||||
request: llama.GenerateChatCompletionRequest(
|
||||
model: model!,
|
||||
messages: history,
|
||||
keepAlive: int.parse(
|
||||
prefs!.getString("keepAlive") ??
|
||||
"300")),
|
||||
)
|
||||
.timeout(const Duration(seconds: 30));
|
||||
|
||||
String text = "";
|
||||
await for (final res in stream) {
|
||||
text += (res.message?.content ?? "");
|
||||
for (var i = 0; i < messages.length; i++) {
|
||||
if (messages[i].id == newId) {
|
||||
messages.removeAt(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (chatAllowed) return;
|
||||
if (text.trim() == "") {
|
||||
throw Exception();
|
||||
}
|
||||
messages.insert(
|
||||
0,
|
||||
types.TextMessage(
|
||||
author: assistant,
|
||||
id: newId,
|
||||
text: text));
|
||||
setState(() {});
|
||||
lightHaptic();
|
||||
}
|
||||
} else {
|
||||
llama.GenerateChatCompletionResponse request;
|
||||
request = await client
|
||||
.generateChatCompletion(
|
||||
request: llama.GenerateChatCompletionRequest(
|
||||
model: model!,
|
||||
messages: history,
|
||||
keepAlive: int.parse(
|
||||
prefs!.getString("keepAlive") ??
|
||||
"300")),
|
||||
)
|
||||
.timeout(const Duration(seconds: 30));
|
||||
if (chatAllowed) return;
|
||||
if (request.message!.content.trim() == "") {
|
||||
throw Exception();
|
||||
}
|
||||
messages.insert(
|
||||
0,
|
||||
types.TextMessage(
|
||||
author: assistant,
|
||||
id: newId,
|
||||
text: request.message!.content));
|
||||
setState(() {});
|
||||
lightHaptic();
|
||||
}
|
||||
} catch (e) {
|
||||
for (var i = 0; i < messages.length; i++) {
|
||||
if (messages[i].id == newId) {
|
||||
messages.removeAt(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
setState(() {
|
||||
chatAllowed = true;
|
||||
messages.removeAt(0);
|
||||
if (messages.isEmpty) {
|
||||
var tmp = (prefs!.getStringList("chats") ?? []);
|
||||
chatUuid = null;
|
||||
for (var i = 0; i < tmp.length; i++) {
|
||||
if (jsonDecode((prefs!.getStringList("chats") ??
|
||||
[])[i])["uuid"] ==
|
||||
chatUuid) {
|
||||
tmp.removeAt(i);
|
||||
prefs!.setStringList("chats", tmp);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
// ignore: use_build_context_synchronously
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
// ignore: use_build_context_synchronously
|
||||
content: Text(AppLocalizations.of(context)!
|
||||
.settingsHostInvalid("timeout")),
|
||||
showCloseIcon: true));
|
||||
return;
|
||||
}
|
||||
|
||||
saveChat(chatUuid!, setState);
|
||||
|
||||
if (newChat &&
|
||||
(prefs!.getBool("generateTitles") ?? true)) {
|
||||
void setTitle() async {
|
||||
List<Map<String, String>> history = [];
|
||||
for (var i = 0; i < messages.length; i++) {
|
||||
if (jsonDecode(jsonEncode(messages[i]))["text"] ==
|
||||
null) {
|
||||
continue;
|
||||
}
|
||||
history.add({
|
||||
"role": (messages[i].author == user)
|
||||
? "user"
|
||||
: "assistant",
|
||||
"content":
|
||||
jsonDecode(jsonEncode(messages[i]))["text"]
|
||||
});
|
||||
}
|
||||
history = history.reversed.toList();
|
||||
|
||||
try {
|
||||
final generated = await client
|
||||
.generateCompletion(
|
||||
request: llama.GenerateCompletionRequest(
|
||||
model: model!,
|
||||
prompt:
|
||||
"You must not use markdown or any other formatting language! Create a short title for the subject of the conversation described in the following json object. It is not allowed to be too general; no 'Assistance', 'Help' or similar!\n\n```json\n${jsonEncode(history)}\n```",
|
||||
keepAlive: int.parse(
|
||||
prefs!.getString("keepAlive") ??
|
||||
"300")),
|
||||
)
|
||||
.timeout(const Duration(seconds: 10));
|
||||
var title = generated.response!
|
||||
.replaceAll("\"", "")
|
||||
.replaceAll("'", "")
|
||||
.replaceAll("*", "")
|
||||
.replaceAll("_", "")
|
||||
.trim();
|
||||
var tmp = (prefs!.getStringList("chats") ?? []);
|
||||
for (var i = 0; i < tmp.length; i++) {
|
||||
if (jsonDecode((prefs!.getStringList("chats") ??
|
||||
[])[i])["uuid"] ==
|
||||
chatUuid) {
|
||||
var tmp2 = jsonDecode(tmp[i]);
|
||||
tmp2["title"] = title;
|
||||
tmp[i] = jsonEncode(tmp2);
|
||||
break;
|
||||
}
|
||||
}
|
||||
prefs!.setStringList("chats", tmp);
|
||||
} catch (_) {}
|
||||
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
setTitle();
|
||||
}
|
||||
|
||||
setState(() {});
|
||||
chatAllowed = true;
|
||||
onSendPressed: (p0) {
|
||||
send(p0.text, context, setState);
|
||||
},
|
||||
onMessageDoubleTap: (context, p1) {
|
||||
selectionHaptic();
|
||||
|
@ -1408,7 +1167,20 @@ class _MainAppState extends State<MainApp> {
|
|||
setState(() {});
|
||||
},
|
||||
onAttachmentPressed: (!multimodal)
|
||||
? null
|
||||
? (prefs?.getBool("voiceModeEnabled") ?? false)
|
||||
? (model != null)
|
||||
? () {
|
||||
selectionHaptic();
|
||||
setMainState = setState;
|
||||
settingsOpen = true;
|
||||
logoVisible = false;
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
const ScreenVoice()));
|
||||
}
|
||||
: null
|
||||
: null
|
||||
: () {
|
||||
selectionHaptic();
|
||||
if (!chatAllowed || model == null) return;
|
||||
|
@ -1455,9 +1227,28 @@ class _MainAppState extends State<MainApp> {
|
|||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () async {
|
||||
HapticFeedback
|
||||
.selectionClick();
|
||||
onPressed: () async {selectionHaptic();
|
||||
Navigator.of(context)
|
||||
.pop();
|
||||
setMainState = setState;
|
||||
settingsOpen = true;
|
||||
logoVisible = false;
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder:
|
||||
(context) =>
|
||||
const ScreenVoice()));
|
||||
},
|
||||
icon: const Icon(Icons
|
||||
.headphones_rounded),
|
||||
label: Text(AppLocalizations
|
||||
.of(context)!
|
||||
.settingsTitleVoice))),
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () async {selectionHaptic();
|
||||
|
||||
Navigator.of(context)
|
||||
.pop();
|
||||
|
@ -1497,8 +1288,7 @@ class _MainAppState extends State<MainApp> {
|
|||
messages.insert(
|
||||
0, message);
|
||||
setState(() {});
|
||||
HapticFeedback
|
||||
.selectionClick();
|
||||
selectionHaptic();
|
||||
},
|
||||
icon: const Icon(Icons
|
||||
.photo_camera_rounded),
|
||||
|
@ -1510,9 +1300,7 @@ class _MainAppState extends State<MainApp> {
|
|||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () async {
|
||||
HapticFeedback
|
||||
.selectionClick();
|
||||
onPressed: () async {selectionHaptic();
|
||||
|
||||
Navigator.of(context)
|
||||
.pop();
|
||||
|
@ -1552,8 +1340,7 @@ class _MainAppState extends State<MainApp> {
|
|||
messages.insert(
|
||||
0, message);
|
||||
setState(() {});
|
||||
HapticFeedback
|
||||
.selectionClick();
|
||||
selectionHaptic();
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.image_rounded),
|
||||
|
@ -1589,8 +1376,12 @@ class _MainAppState extends State<MainApp> {
|
|||
(theme ?? ThemeData()).colorScheme.surface,
|
||||
primaryColor:
|
||||
(theme ?? ThemeData()).colorScheme.primary,
|
||||
attachmentButtonIcon:
|
||||
const Icon(Icons.add_a_photo_rounded),
|
||||
attachmentButtonIcon: !multimodal
|
||||
? (prefs?.getBool("voiceModeEnabled") ??
|
||||
false)
|
||||
? const Icon(Icons.headphones_rounded)
|
||||
: null
|
||||
: const Icon(Icons.add_a_photo_rounded),
|
||||
sendButtonIcon: SizedBox(
|
||||
height: 24,
|
||||
child: CircleAvatar(
|
||||
|
@ -1635,7 +1426,11 @@ class _MainAppState extends State<MainApp> {
|
|||
backgroundColor: (themeDark ?? ThemeData.dark()).colorScheme.surface,
|
||||
primaryColor: (themeDark ?? ThemeData.dark()).colorScheme.primary.withAlpha(40),
|
||||
secondaryColor: (themeDark ?? ThemeData.dark()).colorScheme.primary.withAlpha(20),
|
||||
attachmentButtonIcon: const Icon(Icons.add_a_photo_rounded),
|
||||
attachmentButtonIcon: !multimodal
|
||||
? (prefs?.getBool("voiceModeEnabled") ?? false)
|
||||
? const Icon(Icons.headphones_rounded)
|
||||
: null
|
||||
: const Icon(Icons.add_a_photo_rounded),
|
||||
sendButtonIcon: SizedBox(
|
||||
height: 24,
|
||||
child: CircleAvatar(
|
||||
|
|
|
@ -10,6 +10,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
|||
|
||||
import 'settings/behavior.dart';
|
||||
import 'settings/interface.dart';
|
||||
import 'settings/voice.dart';
|
||||
import 'settings/export.dart';
|
||||
import 'settings/about.dart';
|
||||
|
||||
|
@ -18,7 +19,11 @@ import 'package:http/http.dart' as http;
|
|||
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||
|
||||
Widget toggle(BuildContext context, String text, bool value,
|
||||
Function(bool value) onChanged) {
|
||||
Function(bool value) onChanged,
|
||||
{bool disabled = false,
|
||||
void Function()? onDisabledTap,
|
||||
void Function()? onLongTap,
|
||||
void Function()? onDoubleTap}) {
|
||||
var space = ""; // Invisible character: U+2063
|
||||
var spacePlus = " $space";
|
||||
return InkWell(
|
||||
|
@ -26,8 +31,17 @@ Widget toggle(BuildContext context, String text, bool value,
|
|||
highlightColor: Colors.transparent,
|
||||
hoverColor: Colors.transparent,
|
||||
onTap: () {
|
||||
onChanged(!value);
|
||||
if (disabled) {
|
||||
selectionHaptic();
|
||||
if (onDisabledTap != null) {
|
||||
onDisabledTap();
|
||||
}
|
||||
} else {
|
||||
onChanged(!value);
|
||||
}
|
||||
},
|
||||
onLongPress: onLongTap,
|
||||
onDoubleTap: onDoubleTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 4, bottom: 4),
|
||||
child: Stack(children: [
|
||||
|
@ -43,6 +57,7 @@ Widget toggle(BuildContext context, String text, bool value,
|
|||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
color: disabled ? Colors.grey : null,
|
||||
backgroundColor:
|
||||
(Theme.of(context).brightness == Brightness.light)
|
||||
? (theme ?? ThemeData()).colorScheme.surface
|
||||
|
@ -56,7 +71,22 @@ Widget toggle(BuildContext context, String text, bool value,
|
|||
: (themeDark ?? ThemeData.dark()).colorScheme.surface,
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
child: Switch(value: value, onChanged: onChanged)))
|
||||
child: Switch(
|
||||
value: value,
|
||||
onChanged: disabled
|
||||
? (p0) {
|
||||
selectionHaptic();
|
||||
if (onDisabledTap != null) {
|
||||
onDisabledTap();
|
||||
}
|
||||
}
|
||||
: onChanged,
|
||||
trackOutlineColor: disabled
|
||||
? const WidgetStatePropertyAll(Colors.grey)
|
||||
: null,
|
||||
thumbColor: disabled
|
||||
? const WidgetStatePropertyAll(Colors.grey)
|
||||
: null)))
|
||||
]),
|
||||
]),
|
||||
),
|
||||
|
@ -86,17 +116,36 @@ Widget titleDivider({double top = 16, double bottom = 16}) {
|
|||
));
|
||||
}
|
||||
|
||||
Widget button(String text, IconData icon, void Function()? onPressed,
|
||||
{Color? color}) {
|
||||
Widget button(String text, IconData? icon, void Function()? onPressed,
|
||||
{Color? color,
|
||||
bool disabled = false,
|
||||
void Function()? onDisabledTap,
|
||||
void Function()? onLongTap,
|
||||
void Function()? onDoubleTap}) {
|
||||
return InkWell(
|
||||
onTap: onPressed,
|
||||
onTap: disabled
|
||||
? () {
|
||||
selectionHaptic();
|
||||
if (onDisabledTap != null) {
|
||||
onDisabledTap();
|
||||
}
|
||||
}
|
||||
: onPressed,
|
||||
onLongPress: onLongTap,
|
||||
onDoubleTap: onDoubleTap,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Row(children: [
|
||||
Icon(icon, color: color),
|
||||
const SizedBox(width: 16, height: 42),
|
||||
Expanded(child: Text(text, style: TextStyle(color: color)))
|
||||
(icon != null)
|
||||
? Icon(icon, color: disabled ? Colors.grey : color)
|
||||
: const SizedBox.shrink(),
|
||||
(icon != null)
|
||||
? const SizedBox(width: 16, height: 42)
|
||||
: const SizedBox.shrink(),
|
||||
Expanded(
|
||||
child: Text(text,
|
||||
style: TextStyle(color: disabled ? Colors.grey : color)))
|
||||
]),
|
||||
));
|
||||
}
|
||||
|
@ -389,6 +438,21 @@ class _ScreenSettingsState extends State<ScreenSettings> {
|
|||
builder: (context) =>
|
||||
const ScreenSettingsInterface()));
|
||||
}),
|
||||
(!(Platform.isWindows ||
|
||||
Platform.isLinux ||
|
||||
Platform.isMacOS))
|
||||
? button(
|
||||
AppLocalizations.of(context)!
|
||||
.settingsTitleVoice,
|
||||
Icons.headphones_rounded, () {
|
||||
selectionHaptic();
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
const ScreenSettingsVoice()));
|
||||
})
|
||||
: const SizedBox.shrink(),
|
||||
button(
|
||||
AppLocalizations.of(context)!.settingsTitleExport,
|
||||
Icons.share_rounded, () {
|
||||
|
|
|
@ -0,0 +1,389 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:ollama_app/worker/haptic.dart';
|
||||
import 'package:ollama_app/worker/setter.dart';
|
||||
|
||||
import 'package:speech_to_text/speech_to_text.dart' as stt;
|
||||
import 'package:ollama_dart/ollama_dart.dart' as llama;
|
||||
import 'package:datetime_loop/datetime_loop.dart';
|
||||
|
||||
import 'main.dart';
|
||||
import 'worker/sender.dart';
|
||||
import 'settings/voice.dart';
|
||||
|
||||
class ScreenVoice extends StatefulWidget {
|
||||
const ScreenVoice({super.key});
|
||||
|
||||
@override
|
||||
State<ScreenVoice> createState() => _ScreenVoiceState();
|
||||
}
|
||||
|
||||
class _ScreenVoiceState extends State<ScreenVoice> {
|
||||
Iterable<String> languageOptionIds = [];
|
||||
Iterable<String> languageOptions = [];
|
||||
|
||||
bool speaking = false;
|
||||
bool aiThinking = false;
|
||||
|
||||
bool sttDone = true;
|
||||
String text = "";
|
||||
String aiText = "";
|
||||
|
||||
bool intendedStop = false;
|
||||
|
||||
void setBrightness() {
|
||||
WidgetsBinding.instance.platformDispatcher.onPlatformBrightnessChanged =
|
||||
() {
|
||||
// invert colors used, because brightness not updated yet
|
||||
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
|
||||
systemNavigationBarColor:
|
||||
(prefs!.getString("brightness") ?? "system") == "system"
|
||||
? ((MediaQuery.of(context).platformBrightness ==
|
||||
Brightness.light)
|
||||
? (themeDark ?? ThemeData.dark()).colorScheme.surface
|
||||
: (theme ?? ThemeData()).colorScheme.surface)
|
||||
: (prefs!.getString("brightness") == "dark"
|
||||
? (themeDark ?? ThemeData()).colorScheme.surface
|
||||
: (theme ?? ThemeData.dark()).colorScheme.surface),
|
||||
systemNavigationBarIconBrightness:
|
||||
(((prefs!.getString("brightness") ?? "system") == "system" &&
|
||||
MediaQuery.of(context).platformBrightness ==
|
||||
Brightness.dark) ||
|
||||
prefs!.getString("brightness") == "light")
|
||||
? Brightness.dark
|
||||
: Brightness.light));
|
||||
};
|
||||
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
|
||||
systemNavigationBarColor:
|
||||
(prefs!.getString("brightness") ?? "system") == "system"
|
||||
? ((MediaQuery.of(context).platformBrightness ==
|
||||
Brightness.light)
|
||||
? (theme ?? ThemeData.dark()).colorScheme.surface
|
||||
: (themeDark ?? ThemeData()).colorScheme.surface)
|
||||
: (prefs!.getString("brightness") == "dark"
|
||||
? (themeDark ?? ThemeData()).colorScheme.surface
|
||||
: (theme ?? ThemeData.dark()).colorScheme.surface),
|
||||
systemNavigationBarIconBrightness:
|
||||
(((prefs!.getString("brightness") ?? "system") == "system" &&
|
||||
MediaQuery.of(context).platformBrightness ==
|
||||
Brightness.light) ||
|
||||
prefs!.getString("brightness") == "light")
|
||||
? Brightness.dark
|
||||
: Brightness.light));
|
||||
}
|
||||
|
||||
void process() async {
|
||||
setState(() {
|
||||
speaking = true;
|
||||
sttDone = false;
|
||||
});
|
||||
var textOldOld = text;
|
||||
var textOld = "";
|
||||
text = "";
|
||||
|
||||
speech.listen(
|
||||
localeId: (prefs!.getString("voiceLanguage") ?? ""),
|
||||
listenOptions:
|
||||
stt.SpeechListenOptions(listenMode: stt.ListenMode.dictation),
|
||||
onResult: (result) {
|
||||
lightHaptic();
|
||||
if (!speaking) return;
|
||||
setState(() {
|
||||
sttDone = result.finalResult;
|
||||
text = result.recognizedWords;
|
||||
});
|
||||
},
|
||||
pauseFor: const Duration(seconds: 3));
|
||||
|
||||
DateTime start = DateTime.now();
|
||||
bool timeout = false;
|
||||
await Future.doWhile(() =>
|
||||
Future.delayed(const Duration(milliseconds: 1)).then((_) {
|
||||
if (textOld != text) {
|
||||
start = DateTime.now();
|
||||
}
|
||||
timeout =
|
||||
(DateTime.now().difference(start) >= const Duration(seconds: 3));
|
||||
textOld = text;
|
||||
return !sttDone && speaking && !timeout;
|
||||
}));
|
||||
if (!sttDone || timeout) {
|
||||
sttDone = true;
|
||||
speech.stop();
|
||||
if (timeout) {
|
||||
text = textOldOld;
|
||||
try {
|
||||
setState(() {});
|
||||
} catch (_) {}
|
||||
}
|
||||
if (!intendedStop) {
|
||||
speaking = false;
|
||||
try {
|
||||
setState(() {});
|
||||
} catch (_) {}
|
||||
return;
|
||||
} else {
|
||||
intendedStop = false;
|
||||
try {
|
||||
setState(() {});
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
if (text.isEmpty) {
|
||||
setState(() {
|
||||
speaking = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
aiText = "";
|
||||
heavyHaptic();
|
||||
|
||||
aiThinking = true;
|
||||
try {
|
||||
if (prefs!.getBool("aiPunctuation") ?? true) {
|
||||
final generated = await llama.OllamaClient(
|
||||
headers: (jsonDecode(prefs!.getString("hostHeaders") ?? "{}") as Map)
|
||||
.cast<String, String>(),
|
||||
baseUrl: "$host/api",
|
||||
)
|
||||
.generateCompletion(
|
||||
request: llama.GenerateCompletionRequest(
|
||||
model: model!,
|
||||
prompt:
|
||||
"Add punctuation and syntax to the following sentence. You must not change order of words or a word in itself! You must not add any word or phrase or remove one! Do not change between formal and personal form, keep the original one!\n\n$text",
|
||||
keepAlive: int.parse(prefs!.getString("keepAlive") ?? "300")),
|
||||
)
|
||||
.timeout(const Duration(seconds: 10));
|
||||
setState(() {
|
||||
text = generated.response!;
|
||||
});
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
// ignore: use_build_context_synchronously
|
||||
send(text, context, setState, onStream: (currentText, done) async {
|
||||
setState(() {
|
||||
aiText = currentText;
|
||||
lightHaptic();
|
||||
});
|
||||
|
||||
if (done &&
|
||||
(await voice.getLanguages as List).contains(
|
||||
(prefs!.getString("voiceLanguage") ?? "en_US")
|
||||
.replaceAll("_", "-"))) {
|
||||
aiThinking = false;
|
||||
heavyHaptic();
|
||||
voice.setLanguage((prefs!.getString("voiceLanguage") ?? "en_US")
|
||||
.replaceAll("_", "-"));
|
||||
voice.setSpeechRate(0.6);
|
||||
voice.setCompletionHandler(() async {
|
||||
speaking = false;
|
||||
try {
|
||||
setState(() {});
|
||||
} catch (_) {}
|
||||
process();
|
||||
});
|
||||
var tmp = aiText;
|
||||
tmp.replaceAll("-", ".");
|
||||
tmp.replaceAll("*", ".");
|
||||
voice.speak(tmp);
|
||||
}
|
||||
},
|
||||
addToSystem: (prefs!.getBool("voiceLimitLanguage") ?? true)
|
||||
? "You must write in the following language: ${prefs!.getString("voiceLanguage") ?? "en_US"}!"
|
||||
: null);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
WidgetsBinding.instance.platformDispatcher.onPlatformBrightnessChanged =
|
||||
() {
|
||||
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
|
||||
systemNavigationBarColor:
|
||||
(themeDark ?? ThemeData.dark()).colorScheme.surface,
|
||||
systemNavigationBarIconBrightness: Brightness.dark));
|
||||
};
|
||||
|
||||
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
|
||||
systemNavigationBarColor:
|
||||
(themeDark ?? ThemeData.dark()).colorScheme.surface,
|
||||
systemNavigationBarIconBrightness: Brightness.dark));
|
||||
setState(() {});
|
||||
});
|
||||
|
||||
void load() async {
|
||||
var tmp = await speech.locales();
|
||||
languageOptionIds = tmp.map((e) => e.localeId);
|
||||
languageOptions = tmp.map((e) => e.name);
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
load();
|
||||
|
||||
void loadProcess() async {
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
process();
|
||||
}
|
||||
|
||||
loadProcess();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Theme(
|
||||
data: themeDark!,
|
||||
child: PopScope(
|
||||
canPop: !aiThinking,
|
||||
onPopInvoked: (didPop) {
|
||||
speaking = false;
|
||||
voice.stop();
|
||||
if (chatUuid != null) {
|
||||
loadChat(chatUuid!, setMainState!);
|
||||
}
|
||||
settingsOpen = false;
|
||||
logoVisible = true;
|
||||
setBrightness();
|
||||
},
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
onPressed: () {
|
||||
speaking = false;
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
icon: const Icon(Icons.close_rounded,
|
||||
color: Colors.grey)),
|
||||
title: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(model!.split(":")[0],
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.fade,
|
||||
style: const TextStyle(
|
||||
fontFamily: "monospace", fontSize: 16))),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
speaking = false;
|
||||
settingsOpen = false;
|
||||
logoVisible = true;
|
||||
setBrightness();
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
const ScreenSettingsVoice()));
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.settings_rounded,
|
||||
color: Colors.grey,
|
||||
))
|
||||
]),
|
||||
body: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(mainAxisSize: MainAxisSize.max, children: [
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16),
|
||||
child: Center(
|
||||
child: Text(text,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.fade,
|
||||
style: const TextStyle(
|
||||
color: Colors.grey,
|
||||
fontFamily: "monospace"))),
|
||||
))
|
||||
]),
|
||||
),
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: DateTimeLoopBuilder(
|
||||
timeUnit: TimeUnit.seconds,
|
||||
builder: (context, dateTime, child) {
|
||||
return SizedBox(
|
||||
height: 96,
|
||||
width: 96,
|
||||
child: AnimatedScale(
|
||||
scale: speaking
|
||||
? aiThinking
|
||||
? (dateTime.second).isEven
|
||||
? 2.4
|
||||
: 2
|
||||
: 2
|
||||
: dateTime.second
|
||||
.toString()
|
||||
.endsWith("1")
|
||||
? 1.6
|
||||
: 1.4,
|
||||
duration: aiThinking
|
||||
? const Duration(seconds: 1)
|
||||
: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeInOut,
|
||||
child: InkWell(
|
||||
borderRadius:
|
||||
BorderRadius.circular(48),
|
||||
onTap: () {
|
||||
if (speaking && !aiThinking) {
|
||||
intendedStop = true;
|
||||
speaking = false;
|
||||
voice.stop();
|
||||
return;
|
||||
}
|
||||
process();
|
||||
},
|
||||
child: CircleAvatar(
|
||||
backgroundColor: themeDark!
|
||||
.colorScheme.primary
|
||||
.withAlpha(
|
||||
!speaking ? 200 : 255),
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(
|
||||
milliseconds: 200),
|
||||
child: speaking
|
||||
? aiThinking
|
||||
? Icon(Icons.auto_awesome_rounded,
|
||||
color: themeDark!
|
||||
.colorScheme
|
||||
.secondary,
|
||||
key: const ValueKey(
|
||||
"aiThinking"))
|
||||
: sttDone
|
||||
? Icon(Icons.volume_up_rounded,
|
||||
color: themeDark!
|
||||
.colorScheme
|
||||
.secondary,
|
||||
key: const ValueKey(
|
||||
"tts"))
|
||||
: Icon(Icons.mic_rounded, color: themeDark!.colorScheme.secondary, key: const ValueKey("stt"))
|
||||
: null)))),
|
||||
);
|
||||
}))),
|
||||
Expanded(
|
||||
child: Column(mainAxisSize: MainAxisSize.max, children: [
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16),
|
||||
child: Center(
|
||||
child: Text(aiText,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.fade,
|
||||
style: const TextStyle(
|
||||
fontFamily: "monospace"))),
|
||||
))
|
||||
]),
|
||||
)
|
||||
],
|
||||
))));
|
||||
}
|
||||
}
|
|
@ -106,13 +106,50 @@ class _ScreenWelcomeState extends State<ScreenWelcome> {
|
|||
curve: Curves.easeInOut);
|
||||
} else {
|
||||
prefs!.setBool("welcomeFinished", true);
|
||||
|
||||
WidgetsBinding.instance.platformDispatcher
|
||||
.onPlatformBrightnessChanged = () {
|
||||
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
|
||||
systemNavigationBarColor:
|
||||
(prefs!.getString("brightness") ?? "system") == "system"
|
||||
? ((MediaQuery.of(context).platformBrightness ==
|
||||
Brightness.light)
|
||||
? (themeDark ?? ThemeData.dark())
|
||||
.colorScheme
|
||||
.surface
|
||||
: (theme ?? ThemeData()).colorScheme.surface)
|
||||
: (prefs!.getString("brightness") == "dark"
|
||||
? (themeDark ?? ThemeData()).colorScheme.surface
|
||||
: (theme ?? ThemeData.dark())
|
||||
.colorScheme
|
||||
.surface),
|
||||
systemNavigationBarIconBrightness:
|
||||
(((prefs!.getString("brightness") ?? "system") ==
|
||||
"system" &&
|
||||
MediaQuery.of(context).platformBrightness ==
|
||||
Brightness.dark) ||
|
||||
prefs!.getString("brightness") == "light")
|
||||
? Brightness.dark
|
||||
: Brightness.light));
|
||||
};
|
||||
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
|
||||
systemNavigationBarColor:
|
||||
(Theme.of(context).brightness == Brightness.light)
|
||||
? (theme ?? ThemeData()).colorScheme.surface
|
||||
: (themeDark ?? ThemeData.dark()).colorScheme.surface,
|
||||
(prefs!.getString("brightness") ?? "system") == "system"
|
||||
? ((MediaQuery.of(context).platformBrightness ==
|
||||
Brightness.light)
|
||||
? (theme ?? ThemeData.dark()).colorScheme.surface
|
||||
: (themeDark ?? ThemeData()).colorScheme.surface)
|
||||
: (prefs!.getString("brightness") == "dark"
|
||||
? (themeDark ?? ThemeData()).colorScheme.surface
|
||||
: (theme ?? ThemeData.dark())
|
||||
.colorScheme
|
||||
.surface),
|
||||
systemNavigationBarIconBrightness:
|
||||
(Theme.of(context).brightness == Brightness.light)
|
||||
(((prefs!.getString("brightness") ?? "system") ==
|
||||
"system" &&
|
||||
MediaQuery.of(context).platformBrightness ==
|
||||
Brightness.light) ||
|
||||
prefs!.getString("brightness") == "light")
|
||||
? Brightness.dark
|
||||
: Brightness.light));
|
||||
Navigator.pushReplacement(context,
|
||||
|
|
|
@ -340,8 +340,7 @@ class _ScreenSettingsInterfaceState extends State<ScreenSettingsInterface> {
|
|||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
HapticFeedback
|
||||
.selectionClick();
|
||||
selectionHaptic();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(AppLocalizations.of(
|
||||
|
@ -349,8 +348,7 @@ class _ScreenSettingsInterfaceState extends State<ScreenSettingsInterface> {
|
|||
.settingsBrightnessRestartCancel)),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
HapticFeedback
|
||||
.selectionClick();
|
||||
selectionHaptic();
|
||||
await prefs!.setString(
|
||||
"brightness",
|
||||
p0.elementAt(0));
|
||||
|
|
|
@ -0,0 +1,342 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:ollama_app/worker/haptic.dart';
|
||||
|
||||
import '../main.dart';
|
||||
// import '../worker/haptic.dart';
|
||||
import '../screen_settings.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
class ScreenSettingsVoice extends StatefulWidget {
|
||||
const ScreenSettingsVoice({super.key});
|
||||
|
||||
@override
|
||||
State<ScreenSettingsVoice> createState() => _ScreenSettingsVoiceState();
|
||||
}
|
||||
|
||||
class _ScreenSettingsVoiceState extends State<ScreenSettingsVoice> {
|
||||
bool permissionLoading = true;
|
||||
bool permissionRecord = false;
|
||||
bool permissionBluetooth = false;
|
||||
|
||||
Iterable<String> languageOptionIds = [];
|
||||
Iterable<String> languageOptions = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
void load() async {
|
||||
var tmp = await speech.locales();
|
||||
languageOptionIds = tmp.map((e) => e.localeId);
|
||||
languageOptions = tmp.map((e) => e.name);
|
||||
|
||||
permissionRecord = await Permission.microphone.isGranted;
|
||||
permissionBluetooth = await Permission.bluetoothConnect.isGranted;
|
||||
permissionLoading = false;
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
load();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return WindowBorder(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(AppLocalizations.of(context)!.settingsTitleVoice)),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16),
|
||||
child: Column(children: [
|
||||
Expanded(
|
||||
child: ListView(children: [
|
||||
// const SizedBox(height: 16),
|
||||
((prefs!.getBool("voiceModeEnabled") ?? false) ||
|
||||
permissionLoading ||
|
||||
(permissionBluetooth &&
|
||||
permissionRecord &&
|
||||
voiceSupported))
|
||||
? const SizedBox.shrink()
|
||||
: button(
|
||||
permissionLoading
|
||||
? AppLocalizations.of(context)!
|
||||
.settingsVoicePermissionLoading
|
||||
: (permissionBluetooth && permissionRecord)
|
||||
? AppLocalizations.of(context)!
|
||||
.settingsVoiceNotSupported
|
||||
: AppLocalizations.of(context)!
|
||||
.settingsVoicePermissionNot,
|
||||
Icons.info_rounded, () {
|
||||
if (permissionLoading) return;
|
||||
if (!(permissionBluetooth && permissionRecord)) {
|
||||
void load() async {
|
||||
try {
|
||||
if (await Permission
|
||||
.bluetooth.isPermanentlyDenied ||
|
||||
await Permission
|
||||
.microphone.isPermanentlyDenied) {
|
||||
await openAppSettings();
|
||||
}
|
||||
permissionRecord = await Permission.microphone
|
||||
.request()
|
||||
.isGranted;
|
||||
permissionBluetooth = await Permission
|
||||
.bluetoothConnect
|
||||
.request()
|
||||
.isGranted;
|
||||
permissionLoading = false;
|
||||
|
||||
if (permissionBluetooth && permissionRecord) {
|
||||
voiceSupported = await speech.initialize();
|
||||
}
|
||||
|
||||
setState(() {});
|
||||
} catch (_) {
|
||||
permissionLoading = false;
|
||||
try {
|
||||
setState(() {});
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
load();
|
||||
}
|
||||
}),
|
||||
toggle(
|
||||
context,
|
||||
AppLocalizations.of(context)!.settingsVoiceEnable,
|
||||
(prefs!.getBool("voiceModeEnabled") ?? false), (value) {
|
||||
selectionHaptic();
|
||||
prefs!.setBool("voiceModeEnabled", value);
|
||||
setState(() {});
|
||||
}, disabled: !voiceSupported),
|
||||
button(
|
||||
((prefs!.getString("voiceLanguage") ?? "") == "" ||
|
||||
languageOptions.isEmpty)
|
||||
? AppLocalizations.of(context)!
|
||||
.settingsVoiceNoLanguage
|
||||
: () {
|
||||
for (int i = 0;
|
||||
i < languageOptionIds.length;
|
||||
i++) {
|
||||
if (languageOptionIds.elementAt(i) ==
|
||||
prefs!.getString("voiceLanguage")) {
|
||||
return languageOptions.elementAt(i);
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}(),
|
||||
Icons.language_rounded, () {
|
||||
int usedIndex = -1;
|
||||
Function? setModalState;
|
||||
void load() async {
|
||||
var tmp = await speech.locales();
|
||||
languageOptionIds = tmp.map((e) => e.localeId);
|
||||
languageOptions = tmp.map((e) => e.name);
|
||||
|
||||
if ((prefs!.getString("voiceLanguage") ?? "") != "") {
|
||||
for (int i = 0; i < languageOptionIds.length; i++) {
|
||||
if (languageOptionIds.elementAt(i) ==
|
||||
(prefs!.getString("voiceLanguage") ?? "")) {
|
||||
usedIndex = i;
|
||||
setModalState!(() {});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
selectionHaptic();
|
||||
|
||||
load();
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder:
|
||||
(context) => StatefulBuilder(
|
||||
builder: (context, setLocalState) {
|
||||
setModalState = setLocalState;
|
||||
return PopScope(
|
||||
onPopInvoked: (didPop) {
|
||||
if (usedIndex == -1) return;
|
||||
prefs!.setString(
|
||||
"voiceLanguage",
|
||||
languageOptionIds
|
||||
.elementAt(usedIndex));
|
||||
setState(() {});
|
||||
},
|
||||
child: Container(
|
||||
width: ((Platform.isWindows ||
|
||||
Platform.isLinux ||
|
||||
Platform.isMacOS) &&
|
||||
MediaQuery.of(context)
|
||||
.size
|
||||
.width >=
|
||||
1000)
|
||||
? null
|
||||
: double.infinity,
|
||||
padding: EdgeInsets.only(
|
||||
left: 16,
|
||||
right: 16,
|
||||
top: 16,
|
||||
bottom: (Platform.isWindows ||
|
||||
Platform.isLinux ||
|
||||
Platform.isMacOS)
|
||||
? 16
|
||||
: 0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: ((Platform
|
||||
.isWindows ||
|
||||
Platform
|
||||
.isLinux ||
|
||||
Platform
|
||||
.isMacOS) &&
|
||||
MediaQuery.of(
|
||||
context)
|
||||
.size
|
||||
.width >=
|
||||
1000)
|
||||
? 300
|
||||
: double.infinity,
|
||||
constraints: BoxConstraints(
|
||||
maxHeight:
|
||||
MediaQuery.of(
|
||||
context)
|
||||
.size
|
||||
.height *
|
||||
0.4),
|
||||
child:
|
||||
SingleChildScrollView(
|
||||
scrollDirection:
|
||||
Axis.vertical,
|
||||
child: Wrap(
|
||||
spacing: ((Platform.isWindows ||
|
||||
Platform
|
||||
.isLinux ||
|
||||
Platform
|
||||
.isMacOS) &&
|
||||
MediaQuery.of(context).size.width >=
|
||||
1000)
|
||||
? 10.0
|
||||
: 5.0,
|
||||
runSpacing: (Platform.isWindows ||
|
||||
Platform
|
||||
.isLinux ||
|
||||
Platform
|
||||
.isMacOS)
|
||||
? (MediaQuery.of(context).size.width >=
|
||||
1000)
|
||||
? 10.0
|
||||
: 5.0
|
||||
: 0.0,
|
||||
alignment:
|
||||
WrapAlignment
|
||||
.center,
|
||||
children: List<
|
||||
Widget>.generate(
|
||||
languageOptionIds
|
||||
.length,
|
||||
(int index) {
|
||||
return ChoiceChip(
|
||||
label: Text(
|
||||
languageOptions
|
||||
.elementAt(index)),
|
||||
selected:
|
||||
usedIndex ==
|
||||
index,
|
||||
avatar: (usedIndex ==
|
||||
index)
|
||||
? null
|
||||
: (languageOptionIds.elementAt(index).startsWith(AppLocalizations.of(context)!.localeName))
|
||||
? const Icon(Icons.star_rounded)
|
||||
: null,
|
||||
checkmarkColor: (usedIndex ==
|
||||
index)
|
||||
? ((MediaQuery.of(context).platformBrightness == Brightness.light)
|
||||
? (theme ?? ThemeData()).colorScheme.secondary
|
||||
: (themeDark ?? ThemeData.dark()).colorScheme.secondary)
|
||||
: null,
|
||||
labelStyle: (usedIndex ==
|
||||
index)
|
||||
? TextStyle(
|
||||
color: (MediaQuery.of(context).platformBrightness == Brightness.light) ? (theme ?? ThemeData()).colorScheme.secondary : (themeDark ?? ThemeData.dark()).colorScheme.secondary)
|
||||
: null,
|
||||
selectedColor: (MediaQuery.of(context).platformBrightness ==
|
||||
Brightness
|
||||
.light)
|
||||
? (theme ?? ThemeData())
|
||||
.colorScheme
|
||||
.primary
|
||||
: (themeDark ?? ThemeData.dark())
|
||||
.colorScheme
|
||||
.primary,
|
||||
onSelected:
|
||||
(bool
|
||||
selected) {
|
||||
selectionHaptic();
|
||||
setLocalState(
|
||||
() {
|
||||
usedIndex = selected
|
||||
? index
|
||||
: -1;
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
).toList(),
|
||||
)))
|
||||
])));
|
||||
}));
|
||||
},
|
||||
disabled: (!voiceSupported ||
|
||||
!(prefs!.getBool("voiceModeEnabled") ?? false))),
|
||||
titleDivider(),
|
||||
toggle(
|
||||
context,
|
||||
AppLocalizations.of(context)!
|
||||
.settingsVoiceLimitLanguage,
|
||||
(prefs!.getBool("voiceLimitLanguage") ?? true),
|
||||
(value) {
|
||||
selectionHaptic();
|
||||
prefs!.setBool("voiceLimitLanguage", value);
|
||||
setState(() {});
|
||||
},
|
||||
disabled: (!voiceSupported ||
|
||||
!(prefs!.getBool("voiceModeEnabled") ?? false))),
|
||||
toggle(
|
||||
context,
|
||||
AppLocalizations.of(context)!.settingsVoicePunctuation,
|
||||
(prefs!.getBool("aiPunctuation") ?? true), (value) {
|
||||
selectionHaptic();
|
||||
prefs!.setBool("aiPunctuation", value);
|
||||
setState(() {});
|
||||
},
|
||||
disabled: (!voiceSupported ||
|
||||
!(prefs!.getBool("voiceModeEnabled") ?? false)))
|
||||
]),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
button(
|
||||
AppLocalizations.of(context)!
|
||||
.settingsExperimentalBetaFeature,
|
||||
Icons.warning_rounded,
|
||||
null,
|
||||
color: Colors.orange, onLongTap: () {
|
||||
selectionHaptic();
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text(AppLocalizations.of(context)!
|
||||
.settingsExperimentalBetaDescription),
|
||||
showCloseIcon: true));
|
||||
})
|
||||
]))),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -18,5 +18,7 @@ void heavyHaptic() {
|
|||
|
||||
void selectionHaptic() {
|
||||
if (!(prefs!.getBool("enableHaptic") ?? true)) return;
|
||||
HapticFeedback.selectionClick();
|
||||
// same name but for better experience, change behavior
|
||||
HapticFeedback.lightImpact();
|
||||
// HapticFeedback.selectionClick();
|
||||
}
|
||||
|
|
|
@ -0,0 +1,253 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'haptic.dart';
|
||||
import 'setter.dart';
|
||||
import '../main.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
||||
import 'package:ollama_dart/ollama_dart.dart' as llama;
|
||||
import 'package:dartx/dartx.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
// ignore: depend_on_referenced_packages
|
||||
import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
|
||||
|
||||
List<String> images = [];
|
||||
Future<List<llama.Message>> getHistory([String? addToSystem]) async {
|
||||
var system = prefs?.getString("system") ?? "You are a helpful assistant";
|
||||
if (prefs!.getBool("noMarkdown") ?? false) {
|
||||
system +=
|
||||
"\nYou must not use markdown or any other formatting language in any way!";
|
||||
}
|
||||
if (addToSystem != null) {
|
||||
system += "\n$addToSystem";
|
||||
}
|
||||
|
||||
List<llama.Message> history = [
|
||||
llama.Message(role: llama.MessageRole.system, content: system)
|
||||
];
|
||||
List<llama.Message> history2 = [];
|
||||
images = [];
|
||||
for (var i = 0; i < messages.length; i++) {
|
||||
if (jsonDecode(jsonEncode(messages[i]))["text"] != null) {
|
||||
history2.add(llama.Message(
|
||||
role: (messages[i].author.id == user.id)
|
||||
? llama.MessageRole.user
|
||||
: llama.MessageRole.system,
|
||||
content: jsonDecode(jsonEncode(messages[i]))["text"],
|
||||
images: (images.isNotEmpty) ? images : null));
|
||||
images = [];
|
||||
} else {
|
||||
var uri = jsonDecode(jsonEncode(messages[i]))["uri"] as String;
|
||||
String content = (uri.startsWith("data:image/png;base64,"))
|
||||
? uri.removePrefix("data:image/png;base64,")
|
||||
: base64.encode(await File(uri).readAsBytes());
|
||||
uri = uri.removePrefix("data:image/png;base64,");
|
||||
images.add(content);
|
||||
}
|
||||
}
|
||||
|
||||
history.addAll(history2.reversed.toList());
|
||||
return history;
|
||||
}
|
||||
|
||||
Future<String> send(String value, BuildContext context, Function setState,
|
||||
{void Function(String currentText, bool done)? onStream,
|
||||
String? addToSystem}) async {
|
||||
selectionHaptic();
|
||||
setState(() {
|
||||
sendable = false;
|
||||
});
|
||||
|
||||
if (host == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text(AppLocalizations.of(context)!.noHostSelected),
|
||||
showCloseIcon: true));
|
||||
return "";
|
||||
}
|
||||
|
||||
if (!chatAllowed || model == null) {
|
||||
if (model == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text(AppLocalizations.of(context)!.noModelSelected),
|
||||
showCloseIcon: true));
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
bool newChat = false;
|
||||
if (chatUuid == null) {
|
||||
newChat = true;
|
||||
chatUuid = const Uuid().v4();
|
||||
prefs!.setStringList(
|
||||
"chats",
|
||||
(prefs!.getStringList("chats") ?? []).append([
|
||||
jsonEncode({
|
||||
"title": AppLocalizations.of(context)!.newChatTitle,
|
||||
"uuid": chatUuid,
|
||||
"messages": []
|
||||
})
|
||||
]).toList());
|
||||
}
|
||||
|
||||
var history = await getHistory(addToSystem);
|
||||
|
||||
history.add(llama.Message(
|
||||
role: llama.MessageRole.user,
|
||||
content: value.trim(),
|
||||
images: (images.isNotEmpty) ? images : null));
|
||||
messages.insert(
|
||||
0,
|
||||
types.TextMessage(
|
||||
author: user, id: const Uuid().v4(), text: value.trim()));
|
||||
|
||||
saveChat(chatUuid!, setState);
|
||||
|
||||
setState(() {});
|
||||
chatAllowed = false;
|
||||
|
||||
String text = "";
|
||||
|
||||
String newId = const Uuid().v4();
|
||||
llama.OllamaClient client = llama.OllamaClient(
|
||||
headers: (jsonDecode(prefs!.getString("hostHeaders") ?? "{}") as Map)
|
||||
.cast<String, String>(),
|
||||
baseUrl: "$host/api");
|
||||
|
||||
try {
|
||||
if ((prefs!.getString("requestType") ?? "stream") == "stream") {
|
||||
final stream = client
|
||||
.generateChatCompletionStream(
|
||||
request: llama.GenerateChatCompletionRequest(
|
||||
model: model!,
|
||||
messages: history,
|
||||
keepAlive: int.parse(prefs!.getString("keepAlive") ?? "300")),
|
||||
)
|
||||
.timeout(const Duration(seconds: 30));
|
||||
|
||||
await for (final res in stream) {
|
||||
text += (res.message?.content ?? "");
|
||||
for (var i = 0; i < messages.length; i++) {
|
||||
if (messages[i].id == newId) {
|
||||
messages.removeAt(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (chatAllowed) return "";
|
||||
if (text.trim() == "") {
|
||||
throw Exception();
|
||||
}
|
||||
messages.insert(
|
||||
0, types.TextMessage(author: assistant, id: newId, text: text));
|
||||
if (onStream != null) {
|
||||
onStream(text, false);
|
||||
}
|
||||
setState(() {});
|
||||
heavyHaptic();
|
||||
}
|
||||
} else {
|
||||
llama.GenerateChatCompletionResponse request;
|
||||
request = await client
|
||||
.generateChatCompletion(
|
||||
request: llama.GenerateChatCompletionRequest(
|
||||
model: model!,
|
||||
messages: history,
|
||||
keepAlive: int.parse(prefs!.getString("keepAlive") ?? "300")),
|
||||
)
|
||||
.timeout(const Duration(seconds: 30));
|
||||
if (chatAllowed) return "";
|
||||
if (request.message!.content.trim() == "") {
|
||||
throw Exception();
|
||||
}
|
||||
messages.insert(
|
||||
0,
|
||||
types.TextMessage(
|
||||
author: assistant, id: newId, text: request.message!.content));
|
||||
text = request.message!.content;
|
||||
setState(() {});
|
||||
heavyHaptic();
|
||||
}
|
||||
} catch (e) {
|
||||
for (var i = 0; i < messages.length; i++) {
|
||||
if (messages[i].id == newId) {
|
||||
messages.removeAt(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
setState(() {
|
||||
chatAllowed = true;
|
||||
messages.removeAt(0);
|
||||
if (messages.isEmpty) {
|
||||
var tmp = (prefs!.getStringList("chats") ?? []);
|
||||
chatUuid = null;
|
||||
for (var i = 0; i < tmp.length; i++) {
|
||||
if (jsonDecode((prefs!.getStringList("chats") ?? [])[i])["uuid"] ==
|
||||
chatUuid) {
|
||||
tmp.removeAt(i);
|
||||
prefs!.setStringList("chats", tmp);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
// ignore: use_build_context_synchronously
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content:
|
||||
// ignore: use_build_context_synchronously
|
||||
Text(AppLocalizations.of(context)!.settingsHostInvalid("timeout")),
|
||||
showCloseIcon: true));
|
||||
return "";
|
||||
}
|
||||
|
||||
if ((prefs!.getString("requestType") ?? "stream") == "stream") {
|
||||
if (onStream != null) {
|
||||
onStream(text, true);
|
||||
}
|
||||
}
|
||||
saveChat(chatUuid!, setState);
|
||||
|
||||
if (newChat && (prefs!.getBool("generateTitles") ?? true)) {
|
||||
void setTitle() async {
|
||||
history = await getHistory();
|
||||
|
||||
try {
|
||||
final generated = await client
|
||||
.generateCompletion(
|
||||
request: llama.GenerateCompletionRequest(
|
||||
model: model!,
|
||||
prompt:
|
||||
"You must not use markdown or any other formatting language! Create a short title for the subject of the conversation described in the following json object. It is not allowed to be too general; no 'Assistance', 'Help' or similar!\n\n```json\n${jsonEncode(history)}\n```",
|
||||
keepAlive: int.parse(prefs!.getString("keepAlive") ?? "300")),
|
||||
)
|
||||
.timeout(const Duration(seconds: 10));
|
||||
var title = generated.response!
|
||||
.replaceAll("\"", "")
|
||||
.replaceAll("'", "")
|
||||
.replaceAll("*", "")
|
||||
.replaceAll("_", "")
|
||||
.trim();
|
||||
var tmp = (prefs!.getStringList("chats") ?? []);
|
||||
for (var i = 0; i < tmp.length; i++) {
|
||||
if (jsonDecode((prefs!.getStringList("chats") ?? [])[i])["uuid"] ==
|
||||
chatUuid) {
|
||||
var tmp2 = jsonDecode(tmp[i]);
|
||||
tmp2["title"] = title;
|
||||
tmp[i] = jsonEncode(tmp2);
|
||||
break;
|
||||
}
|
||||
}
|
||||
prefs!.setStringList("chats", tmp);
|
||||
} catch (_) {}
|
||||
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
setTitle();
|
||||
}
|
||||
|
||||
setState(() {});
|
||||
chatAllowed = true;
|
||||
return text;
|
||||
}
|
|
@ -198,6 +198,7 @@ void setModel(BuildContext context, Function setState) {
|
|||
.colorScheme
|
||||
.primary,
|
||||
onSelected: (bool selected) {
|
||||
selectionHaptic();
|
||||
if (addIndex == index) {
|
||||
Navigator.of(context).pop();
|
||||
ScaffoldMessenger.of(context)
|
||||
|
|
104
pubspec.lock
104
pubspec.lock
|
@ -129,6 +129,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
datetime_loop:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: datetime_loop
|
||||
sha256: "5b7b694879e505368e6d0e04ac8bb55a1e7d2e493dd0b851e08db08e1ba784e2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
diffutil_dart:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -328,6 +336,14 @@ packages:
|
|||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_tts:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_tts
|
||||
sha256: aed2a00c48c43af043ed81145fd8503ddd793dafa7088ab137dbef81a703e53d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.2"
|
||||
flutter_web_plugins:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
|
@ -445,6 +461,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.19.0"
|
||||
js:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: js
|
||||
sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.1"
|
||||
json_annotation:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -590,6 +614,62 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.1"
|
||||
pedantic:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pedantic
|
||||
sha256: "67fc27ed9639506c856c840ccce7594d0bdcd91bc8d53d6e52359449a1d50602"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.11.1"
|
||||
permission_handler:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: permission_handler
|
||||
sha256: "18bf33f7fefbd812f37e72091a15575e72d5318854877e0e4035a24ac1113ecb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "11.3.1"
|
||||
permission_handler_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_android
|
||||
sha256: "8bb852cd759488893805c3161d0b2b5db55db52f773dbb014420b304055ba2c5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "12.0.6"
|
||||
permission_handler_apple:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_apple
|
||||
sha256: e6f6d73b12438ef13e648c4ae56bd106ec60d17e90a59c4545db6781229082a0
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.4.5"
|
||||
permission_handler_html:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_html
|
||||
sha256: "54bf176b90f6eddd4ece307e2c06cf977fb3973719c35a93b85cc7093eb6070d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.1"
|
||||
permission_handler_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_platform_interface
|
||||
sha256: "48d4fcf201a1dad93ee869ab0d4101d084f49136ec82a8a06ed9cfeacab9fd20"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.2.1"
|
||||
permission_handler_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_windows
|
||||
sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.1"
|
||||
photo_view:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -715,6 +795,30 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.10.0"
|
||||
speech_to_text:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: speech_to_text
|
||||
sha256: "97425fd8cc60424061a0584b6c418c0eedab5201cc5e96ef15a946d7fab7b9b7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.6.2"
|
||||
speech_to_text_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: speech_to_text_macos
|
||||
sha256: e685750f7542fcaa087a5396ee471e727ec648bf681f4da83c84d086322173f6
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
speech_to_text_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: speech_to_text_platform_interface
|
||||
sha256: a0df1a907091ea09880077dc25aae02af9f79811264e6e97ddb08639b7f771c2
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
sprintf:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
@ -38,6 +38,10 @@ dependencies:
|
|||
version: ^3.0.2
|
||||
flutter_displaymode: ^0.6.0
|
||||
duration_picker: ^1.2.0
|
||||
speech_to_text: ^6.6.2
|
||||
flutter_tts: ^4.0.2
|
||||
permission_handler: ^11.3.1
|
||||
datetime_loop: ^1.2.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
|
||||
#include <bitsdojo_window_windows/bitsdojo_window_plugin.h>
|
||||
#include <file_selector_windows/file_selector_windows.h>
|
||||
#include <flutter_tts/flutter_tts_plugin.h>
|
||||
#include <permission_handler_windows/permission_handler_windows_plugin.h>
|
||||
#include <url_launcher_windows/url_launcher_windows.h>
|
||||
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
|
@ -15,6 +17,10 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
|||
registry->GetRegistrarForPlugin("BitsdojoWindowPlugin"));
|
||||
FileSelectorWindowsRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
||||
FlutterTtsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FlutterTtsPlugin"));
|
||||
PermissionHandlerWindowsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
|
||||
UrlLauncherWindowsRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||
}
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
bitsdojo_window_windows
|
||||
file_selector_windows
|
||||
flutter_tts
|
||||
permission_handler_windows
|
||||
url_launcher_windows
|
||||
)
|
||||
|
||||
|
|
Loading…
Reference in New Issue