added chat app
This commit is contained in:
252
Chat-App/lib/services/convai_service.dart
Normal file
252
Chat-App/lib/services/convai_service.dart
Normal file
@ -0,0 +1,252 @@
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class ConvaiService {
|
||||
static const String _baseUrl = 'https://api.convai.com/character/getResponse';
|
||||
|
||||
String _apiKey = '';
|
||||
String _characterId = '';
|
||||
String _sessionId = '-1';
|
||||
|
||||
// Remove default values for security - users must set their own
|
||||
static const String defaultApiKey = '';
|
||||
static const String defaultCharacterId = '';
|
||||
|
||||
Future<void> loadSettings() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
_apiKey = prefs.getString('api_key') ?? defaultApiKey;
|
||||
_characterId = prefs.getString('character_id') ?? defaultCharacterId;
|
||||
}
|
||||
|
||||
Future<void> saveSettings(String apiKey, String characterId) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString('api_key', apiKey);
|
||||
await prefs.setString('character_id', characterId);
|
||||
_apiKey = apiKey;
|
||||
_characterId = characterId;
|
||||
}
|
||||
|
||||
String get apiKey => _apiKey;
|
||||
String get characterId => _characterId;
|
||||
|
||||
bool get isConfigured => _apiKey.isNotEmpty && _characterId.isNotEmpty;
|
||||
|
||||
void resetSession() {
|
||||
_sessionId = '-1';
|
||||
}
|
||||
|
||||
Future<String> sendMessage(String userText) async {
|
||||
if (_apiKey.isEmpty || _characterId.isEmpty) {
|
||||
throw Exception('API key or Character ID not set. Please check settings.');
|
||||
}
|
||||
|
||||
try {
|
||||
final request = http.MultipartRequest('POST', Uri.parse(_baseUrl));
|
||||
|
||||
// Add headers
|
||||
request.headers['CONVAI-API-KEY'] = _apiKey;
|
||||
|
||||
// Add form fields
|
||||
request.fields['userText'] = userText;
|
||||
request.fields['charID'] = _characterId;
|
||||
request.fields['sessionID'] = _sessionId;
|
||||
request.fields['voiceResponse'] = 'False';
|
||||
|
||||
final streamedResponse = await request.send();
|
||||
final response = await http.Response.fromStream(streamedResponse);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = json.decode(response.body);
|
||||
_sessionId = data['sessionID'] ?? _sessionId;
|
||||
return data['text'] ?? 'No response received';
|
||||
} else {
|
||||
throw Exception('HTTP ${response.statusCode}: ${response.reasonPhrase}');
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('Failed to send message: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Get list of saved character IDs
|
||||
Future<List<String>> getSavedCharacters() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getStringList('saved_characters') ?? [];
|
||||
}
|
||||
|
||||
// Save a new character ID
|
||||
Future<void> saveCharacter(String characterId) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final characters = await getSavedCharacters();
|
||||
if (!characters.contains(characterId)) {
|
||||
characters.add(characterId);
|
||||
await prefs.setStringList('saved_characters', characters);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove a character ID
|
||||
Future<void> removeCharacter(String characterId) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final characters = await getSavedCharacters();
|
||||
characters.remove(characterId);
|
||||
await prefs.setStringList('saved_characters', characters);
|
||||
|
||||
// Clean up name and initial prompt entries for this character
|
||||
final names = await _getCharacterNamesMap();
|
||||
if (names.remove(characterId) != null) {
|
||||
await _setCharacterNamesMap(names);
|
||||
}
|
||||
final prompts = await _getCharacterInitialPromptsMap();
|
||||
if (prompts.remove(characterId) != null) {
|
||||
await _setCharacterInitialPromptsMap(prompts);
|
||||
}
|
||||
}
|
||||
|
||||
// Rename an existing character ID and migrate associated name and initial prompt
|
||||
Future<void> renameCharacterId(String oldCharacterId, String newCharacterId) async {
|
||||
final oldId = oldCharacterId.trim();
|
||||
final newId = newCharacterId.trim();
|
||||
if (newId.isEmpty) {
|
||||
throw Exception('New character ID cannot be empty');
|
||||
}
|
||||
if (oldId == newId) return;
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final characters = await getSavedCharacters();
|
||||
if (!characters.contains(oldId)) {
|
||||
throw Exception('Character not found');
|
||||
}
|
||||
if (characters.contains(newId)) {
|
||||
throw Exception('A character with this ID already exists');
|
||||
}
|
||||
|
||||
// Replace in list preserving order
|
||||
final index = characters.indexOf(oldId);
|
||||
characters[index] = newId;
|
||||
await prefs.setStringList('saved_characters', characters);
|
||||
|
||||
// Migrate name
|
||||
final names = await _getCharacterNamesMap();
|
||||
if (names.containsKey(oldId)) {
|
||||
names[newId] = names[oldId] ?? '';
|
||||
names.remove(oldId);
|
||||
await _setCharacterNamesMap(names);
|
||||
}
|
||||
|
||||
// Migrate initial prompt
|
||||
final prompts = await _getCharacterInitialPromptsMap();
|
||||
if (prompts.containsKey(oldId)) {
|
||||
prompts[newId] = prompts[oldId] ?? '';
|
||||
prompts.remove(oldId);
|
||||
await _setCharacterInitialPromptsMap(prompts);
|
||||
}
|
||||
|
||||
// Update current configured character if it matches
|
||||
if (_characterId == oldId) {
|
||||
await prefs.setString('character_id', newId);
|
||||
_characterId = newId;
|
||||
}
|
||||
}
|
||||
|
||||
// Character names management
|
||||
Future<Map<String, String>> _getCharacterNamesMap() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final raw = prefs.getString('character_names_json');
|
||||
if (raw == null || raw.isEmpty) return {};
|
||||
try {
|
||||
final decoded = json.decode(raw) as Map<String, dynamic>;
|
||||
return decoded.map((k, v) => MapEntry(k, v?.toString() ?? ''));
|
||||
} catch (_) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _setCharacterNamesMap(Map<String, String> map) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString('character_names_json', json.encode(map));
|
||||
}
|
||||
|
||||
Future<String> getCharacterName(String characterId) async {
|
||||
final map = await _getCharacterNamesMap();
|
||||
return map[characterId] ?? '';
|
||||
}
|
||||
|
||||
Future<void> setCharacterName(String characterId, String name) async {
|
||||
final map = await _getCharacterNamesMap();
|
||||
if (name.isEmpty) {
|
||||
map.remove(characterId);
|
||||
} else {
|
||||
map[characterId] = name;
|
||||
}
|
||||
await _setCharacterNamesMap(map);
|
||||
}
|
||||
|
||||
Future<Map<String, String>> getAllCharacterNames() async {
|
||||
return _getCharacterNamesMap();
|
||||
}
|
||||
|
||||
// Character initial prompts management
|
||||
Future<Map<String, String>> _getCharacterInitialPromptsMap() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final raw = prefs.getString('character_initial_prompts_json');
|
||||
if (raw == null || raw.isEmpty) return {};
|
||||
try {
|
||||
final decoded = json.decode(raw) as Map<String, dynamic>;
|
||||
return decoded.map((k, v) => MapEntry(k, v?.toString() ?? ''));
|
||||
} catch (_) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _setCharacterInitialPromptsMap(Map<String, String> map) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString('character_initial_prompts_json', json.encode(map));
|
||||
}
|
||||
|
||||
Future<String> getCharacterInitialPrompt(String characterId) async {
|
||||
final map = await _getCharacterInitialPromptsMap();
|
||||
return map[characterId] ?? '';
|
||||
}
|
||||
|
||||
Future<void> setCharacterInitialPrompt(String characterId, String prompt) async {
|
||||
final map = await _getCharacterInitialPromptsMap();
|
||||
if (prompt.isEmpty) {
|
||||
map.remove(characterId);
|
||||
} else {
|
||||
map[characterId] = prompt;
|
||||
}
|
||||
await _setCharacterInitialPromptsMap(map);
|
||||
}
|
||||
|
||||
// Supervised mode
|
||||
Future<bool> isSupervisedModeEnabled() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getBool('supervised_mode') ?? false;
|
||||
}
|
||||
|
||||
Future<void> setSupervisedModeEnabled(bool enabled) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool('supervised_mode', enabled);
|
||||
}
|
||||
|
||||
// Timed experiment settings
|
||||
Future<bool> isTimedExperimentEnabled() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getBool('timed_experiment_enabled') ?? false;
|
||||
}
|
||||
|
||||
Future<void> setTimedExperimentEnabled(bool enabled) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool('timed_experiment_enabled', enabled);
|
||||
}
|
||||
|
||||
Future<int> getExperimentDurationMinutes() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getInt('experiment_duration_minutes') ?? 5;
|
||||
}
|
||||
|
||||
Future<void> setExperimentDurationMinutes(int minutes) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setInt('experiment_duration_minutes', minutes);
|
||||
}
|
||||
}
|
||||
50
Chat-App/lib/services/deepl_service.dart
Normal file
50
Chat-App/lib/services/deepl_service.dart
Normal file
@ -0,0 +1,50 @@
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
class DeepLService {
|
||||
Future<String?> translateText({
|
||||
required String apiKey,
|
||||
required String text,
|
||||
required String targetLang,
|
||||
String sourceLang = 'AUTO',
|
||||
bool useFreeApi = true,
|
||||
}) async {
|
||||
if (apiKey.isEmpty || text.trim().isEmpty) return null;
|
||||
|
||||
final base = useFreeApi ? 'https://api-free.deepl.com' : 'https://api.deepl.com';
|
||||
final uri = Uri.parse('$base/v2/translate');
|
||||
|
||||
try {
|
||||
final response = await http.post(
|
||||
uri,
|
||||
headers: {
|
||||
'Authorization': 'DeepL-Auth-Key $apiKey',
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: {
|
||||
'text': text,
|
||||
'target_lang': targetLang,
|
||||
if (sourceLang.isNotEmpty && sourceLang.toUpperCase() != 'AUTO')
|
||||
'source_lang': sourceLang,
|
||||
'preserve_formatting': '1',
|
||||
},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final jsonBody = json.decode(utf8.decode(response.bodyBytes));
|
||||
final translations = jsonBody['translations'] as List<dynamic>?;
|
||||
if (translations != null && translations.isNotEmpty) {
|
||||
return translations.first['text']?.toString();
|
||||
}
|
||||
return null;
|
||||
} else {
|
||||
// Swallow errors and return null to avoid blocking the main save flow
|
||||
return null;
|
||||
}
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
97
Chat-App/lib/services/storage_service.dart
Normal file
97
Chat-App/lib/services/storage_service.dart
Normal file
@ -0,0 +1,97 @@
|
||||
import 'dart:io';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:ai_chat_lab/services/deepl_service.dart';
|
||||
|
||||
class StorageService {
|
||||
|
||||
Future<bool> isSavingEnabled() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getBool('save_conversations') ?? false;
|
||||
}
|
||||
|
||||
Future<void> setSavingEnabled(bool enabled) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool('save_conversations', enabled);
|
||||
}
|
||||
|
||||
Future<void> saveConversation(String participantId, String userMessage, String agentResponse) async {
|
||||
if (!await isSavingEnabled()) return;
|
||||
|
||||
try {
|
||||
final directory = await getApplicationDocumentsDirectory();
|
||||
final chatDir = Directory('${directory.path}/convai_chats');
|
||||
|
||||
if (!await chatDir.exists()) {
|
||||
await chatDir.create(recursive: true);
|
||||
}
|
||||
|
||||
final timestamp = DateTime.now();
|
||||
final dateStr = '${timestamp.year}-${timestamp.month.toString().padLeft(2, '0')}-${timestamp.day.toString().padLeft(2, '0')}';
|
||||
final baseName = 'chat_${participantId.isNotEmpty ? participantId : 'default'}_$dateStr';
|
||||
final file = File('${chatDir.path}/$baseName.txt');
|
||||
|
||||
final timeStr = '${timestamp.hour.toString().padLeft(2, '0')}:${timestamp.minute.toString().padLeft(2, '0')}:${timestamp.second.toString().padLeft(2, '0')}';
|
||||
final entry = '\n[$timeStr]\nparticipant: "$userMessage"\nagent: "$agentResponse"\n';
|
||||
|
||||
await file.writeAsString(entry, mode: FileMode.append);
|
||||
|
||||
// Also save DeepL-translated variant if enabled/configured
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final deeplEnabled = prefs.getBool('deepl_enabled') ?? false;
|
||||
final deeplApiKey = prefs.getString('deepl_api_key') ?? '';
|
||||
final deeplUseFree = prefs.getBool('deepl_use_free') ?? true;
|
||||
final deeplTargetLang = (prefs.getString('deepl_target_lang') ?? 'EN').toUpperCase();
|
||||
final deeplSourceLang = (prefs.getString('deepl_source_lang') ?? 'AUTO').toUpperCase();
|
||||
|
||||
if (deeplEnabled && deeplApiKey.isNotEmpty) {
|
||||
final deepl = DeepLService();
|
||||
final translatedUser = await deepl.translateText(
|
||||
apiKey: deeplApiKey,
|
||||
text: userMessage,
|
||||
targetLang: deeplTargetLang,
|
||||
sourceLang: deeplSourceLang,
|
||||
useFreeApi: deeplUseFree,
|
||||
);
|
||||
final translatedAgent = await deepl.translateText(
|
||||
apiKey: deeplApiKey,
|
||||
text: agentResponse,
|
||||
targetLang: deeplTargetLang,
|
||||
sourceLang: deeplSourceLang,
|
||||
useFreeApi: deeplUseFree,
|
||||
);
|
||||
|
||||
if ((translatedUser ?? '').isNotEmpty || (translatedAgent ?? '').isNotEmpty) {
|
||||
final translatedFile = File('${chatDir.path}/$baseName.${deeplTargetLang.toLowerCase()}.txt');
|
||||
final entryTranslated = '\n[$timeStr]\nparticipant: "${translatedUser ?? userMessage}"\nagent: "${translatedAgent ?? agentResponse}"\n';
|
||||
await translatedFile.writeAsString(entryTranslated, mode: FileMode.append);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error saving conversation: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<File>> getChatFiles() async {
|
||||
try {
|
||||
final directory = await getApplicationDocumentsDirectory();
|
||||
final chatDir = Directory('${directory.path}/convai_chats');
|
||||
|
||||
if (!await chatDir.exists()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
final files = await chatDir.list().where((entity) => entity is File && entity.path.endsWith('.txt')).cast<File>().toList();
|
||||
files.sort((a, b) => b.lastModifiedSync().compareTo(a.lastModifiedSync()));
|
||||
return files;
|
||||
} catch (e) {
|
||||
print('Error getting chat files: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> getChatDirectory() async {
|
||||
final directory = await getApplicationDocumentsDirectory();
|
||||
return '${directory.path}/convai_chats';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user