Mudança nos icones de ios.
@@ -1,6 +1,6 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application
|
||||
android:label="app"
|
||||
android:label="EPVChat"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
<activity
|
||||
|
||||
|
Before Width: | Height: | Size: 544 B After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 442 B After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 721 B After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 7.8 KiB |
BIN
assets/icon/icon_logos.png
Normal file
|
After Width: | Height: | Size: 95 KiB |
@@ -431,7 +431,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
@@ -488,7 +488,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
|
||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 295 B After Width: | Height: | Size: 538 B |
|
Before Width: | Height: | Size: 406 B After Width: | Height: | Size: 885 B |
|
Before Width: | Height: | Size: 450 B After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 282 B After Width: | Height: | Size: 672 B |
|
Before Width: | Height: | Size: 462 B After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 704 B After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 406 B After Width: | Height: | Size: 885 B |
|
Before Width: | Height: | Size: 586 B After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 862 B After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 862 B After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 762 B After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 6.0 KiB |
@@ -4,6 +4,7 @@ import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'dart:async';
|
||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
import 'database_helper.dart';
|
||||
|
||||
class ChatScreen extends StatefulWidget {
|
||||
const ChatScreen({super.key});
|
||||
@@ -12,17 +13,15 @@ class ChatScreen extends StatefulWidget {
|
||||
State<ChatScreen> createState() => _ChatScreenState();
|
||||
}
|
||||
|
||||
class ChatMessage {
|
||||
final String text;
|
||||
final bool isAssistant;
|
||||
|
||||
ChatMessage({required this.text, this.isAssistant = false});
|
||||
}
|
||||
|
||||
class _ChatScreenState extends State<ChatScreen> with TickerProviderStateMixin {
|
||||
final List<ChatMessage> _messages = [];
|
||||
final List<ChatSession> _sessions = [];
|
||||
ChatSession? _currentSession;
|
||||
|
||||
final TextEditingController _textController = TextEditingController();
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
|
||||
bool _isTyping = false;
|
||||
late AnimationController _typingController;
|
||||
|
||||
@@ -34,26 +33,62 @@ class _ChatScreenState extends State<ChatScreen> with TickerProviderStateMixin {
|
||||
duration: const Duration(milliseconds: 1200),
|
||||
)..repeat();
|
||||
|
||||
Timer(const Duration(seconds: 2), () => _checkAvailableModels());
|
||||
_loadInitialData();
|
||||
}
|
||||
|
||||
Future<void> _checkAvailableModels() async {
|
||||
try {
|
||||
final response = await http.get(
|
||||
Uri.parse('http://89.114.196.110:11434/api/tags'),
|
||||
).timeout(const Duration(seconds: 15));
|
||||
Future<void> _loadInitialData() async {
|
||||
await _loadSessions();
|
||||
if (_sessions.isNotEmpty) {
|
||||
await _selectSession(_sessions.first);
|
||||
} else {
|
||||
await _createNewSession();
|
||||
}
|
||||
}
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = jsonDecode(response.body);
|
||||
print("--- MODELOS DISPONÍVEIS ---");
|
||||
if (data['models'] != null) {
|
||||
for (var m in data['models']) {
|
||||
print("- ${m['name']}");
|
||||
Future<void> _loadSessions() async {
|
||||
final sessions = await DatabaseHelper.instance.getSessions();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_sessions.clear();
|
||||
_sessions.addAll(sessions);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _selectSession(ChatSession session) async {
|
||||
final messages = await DatabaseHelper.instance.getMessages(session.id!);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_currentSession = session;
|
||||
_messages.clear();
|
||||
_messages.addAll(messages.reversed);
|
||||
_isTyping = false;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
print("Erro ao listar modelos: $e");
|
||||
if (_scaffoldKey.currentState?.isDrawerOpen ?? false) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _createNewSession() async {
|
||||
final title = "Chat ${DateTime.now().hour}:${DateTime.now().minute}";
|
||||
final id = await DatabaseHelper.instance.createSession(title);
|
||||
await _loadSessions();
|
||||
final newSession = _sessions.firstWhere((s) => s.id == id);
|
||||
await _selectSession(newSession);
|
||||
}
|
||||
|
||||
Future<void> _deleteSession(int id) async {
|
||||
await DatabaseHelper.instance.deleteSession(id);
|
||||
await _loadSessions();
|
||||
if (_currentSession?.id == id) {
|
||||
if (_sessions.isNotEmpty) {
|
||||
await _selectSession(_sessions.first);
|
||||
} else {
|
||||
await _createNewSession();
|
||||
}
|
||||
} else {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,61 +100,76 @@ class _ChatScreenState extends State<ChatScreen> with TickerProviderStateMixin {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _clearChat() {
|
||||
setState(() {
|
||||
_messages.clear();
|
||||
_isTyping = false;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _handleSubmitted(String text) async {
|
||||
if (text.trim().isEmpty || _isTyping) return;
|
||||
|
||||
// Se por algum motivo não houver sessão, cria uma antes de enviar
|
||||
if (_currentSession == null) {
|
||||
await _createNewSession();
|
||||
}
|
||||
|
||||
final userMsgText = text.trim();
|
||||
_textController.clear();
|
||||
|
||||
final userMsg = ChatMessage(
|
||||
sessionId: _currentSession!.id!,
|
||||
text: userMsgText,
|
||||
isAssistant: false,
|
||||
timestamp: DateTime.now().toIso8601String(),
|
||||
);
|
||||
|
||||
await DatabaseHelper.instance.insertMessage(userMsg);
|
||||
|
||||
setState(() {
|
||||
_messages.insert(0, ChatMessage(text: text));
|
||||
_messages.insert(0, userMsg);
|
||||
_isTyping = true;
|
||||
});
|
||||
|
||||
try {
|
||||
final url = Uri.parse('http://89.114.196.110:11434/api/chat');
|
||||
|
||||
final response = await http
|
||||
.post(
|
||||
final response = await http.post(
|
||||
url,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: jsonEncode({
|
||||
'model': 'qwen3:4b',
|
||||
'messages': [{'role': 'user', 'content': text}],
|
||||
'messages': [{'role': 'user', 'content': userMsgText}],
|
||||
'stream': false,
|
||||
}),
|
||||
)
|
||||
.timeout(const Duration(seconds: 60));
|
||||
).timeout(const Duration(seconds: 60));
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = jsonDecode(response.body);
|
||||
final reply = data['message']?['content'] ?? 'Sem resposta.';
|
||||
final replyText = data['message']?['content'] ?? 'Sem resposta.';
|
||||
|
||||
final assistantMsg = ChatMessage(
|
||||
sessionId: _currentSession!.id!,
|
||||
text: replyText,
|
||||
isAssistant: true,
|
||||
timestamp: DateTime.now().toIso8601String(),
|
||||
);
|
||||
|
||||
await DatabaseHelper.instance.insertMessage(assistantMsg);
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isTyping = false;
|
||||
_messages.insert(0, ChatMessage(text: reply, isAssistant: true));
|
||||
_messages.insert(0, assistantMsg);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
throw Exception('Erro HTTP ${response.statusCode}: ${response.body}');
|
||||
throw Exception('Erro HTTP ${response.statusCode}');
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isTyping = false;
|
||||
_messages.insert(
|
||||
0,
|
||||
ChatMessage(text: "Erro: $e", isAssistant: true),
|
||||
);
|
||||
_messages.insert(0, ChatMessage(
|
||||
sessionId: _currentSession!.id!,
|
||||
text: "Erro de ligação. Verifique se o servidor está online.",
|
||||
isAssistant: true,
|
||||
timestamp: DateTime.now().toIso8601String(),
|
||||
));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -147,6 +197,9 @@ class _ChatScreenState extends State<ChatScreen> with TickerProviderStateMixin {
|
||||
bottomLeft: Radius.circular(isAssistant ? 4 : 20),
|
||||
bottomRight: Radius.circular(isAssistant ? 20 : 4),
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 5, offset: const Offset(0, 2)),
|
||||
],
|
||||
),
|
||||
child: isAssistant
|
||||
? MarkdownBody(
|
||||
@@ -154,13 +207,9 @@ class _ChatScreenState extends State<ChatScreen> with TickerProviderStateMixin {
|
||||
styleSheet: MarkdownStyleSheet(
|
||||
p: const TextStyle(color: Colors.black87, fontSize: 15, height: 1.4),
|
||||
strong: const TextStyle(color: Colors.black, fontWeight: FontWeight.bold),
|
||||
listBullet: const TextStyle(color: Colors.black87),
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
message.text,
|
||||
style: const TextStyle(color: Colors.white, fontSize: 15, height: 1.4),
|
||||
),
|
||||
: Text(message.text, style: const TextStyle(color: Colors.white, fontSize: 15)),
|
||||
),
|
||||
),
|
||||
if (!isAssistant) _buildAvatar(Icons.person),
|
||||
@@ -218,50 +267,38 @@ class _ChatScreenState extends State<ChatScreen> with TickerProviderStateMixin {
|
||||
}
|
||||
|
||||
Widget _buildTextComposer() {
|
||||
return ClipRRect(
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
|
||||
child: Container(
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.8),
|
||||
border: Border(top: BorderSide(color: Colors.black.withOpacity(0.05), width: 0.5)),
|
||||
color: Colors.white,
|
||||
border: Border(top: BorderSide(color: Colors.black.withOpacity(0.05))),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete_sweep_rounded, color: Colors.redAccent),
|
||||
onPressed: _isTyping ? null : _clearChat,
|
||||
),
|
||||
Expanded(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF1F5F9),
|
||||
borderRadius: BorderRadius.circular(30.0),
|
||||
border: Border.all(color: Colors.black.withOpacity(0.05)),
|
||||
),
|
||||
child: TextField(
|
||||
controller: _textController,
|
||||
enabled: !_isTyping,
|
||||
onSubmitted: _handleSubmitted,
|
||||
decoration: InputDecoration(
|
||||
hintText: _isTyping ? "Aguarde a resposta..." : "Mensagem EPVChat...",
|
||||
decoration: const InputDecoration(
|
||||
hintText: "Mensagem EPVChat...",
|
||||
border: InputBorder.none,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 14.0),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 24.0, vertical: 14.0),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const SizedBox(width: 12),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: _isTyping
|
||||
? [Colors.grey, Colors.grey]
|
||||
: [const Color(0xFF8ad5c9), const Color(0xFF57a7ed)],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: _isTyping ? [Colors.grey, Colors.grey] : [const Color(0xFF8ad5c9), const Color(0xFF57a7ed)],
|
||||
),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
@@ -273,8 +310,6 @@ class _ChatScreenState extends State<ChatScreen> with TickerProviderStateMixin {
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -283,47 +318,125 @@ class _ChatScreenState extends State<ChatScreen> with TickerProviderStateMixin {
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
|
||||
return Scaffold(
|
||||
key: _scaffoldKey,
|
||||
drawer: _buildSidebar(),
|
||||
backgroundColor: Colors.white,
|
||||
body: Column(
|
||||
body: Stack(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ListView(
|
||||
controller: _scrollController,
|
||||
reverse: true,
|
||||
children: [
|
||||
...(_isTyping ? [_buildTypingIndicator()] : []),
|
||||
..._messages.map((msg) => _buildMessage(msg)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 50, top: 20),
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Container(
|
||||
height: screenWidth * 0.8,
|
||||
// Sombreado Mint
|
||||
Positioned(
|
||||
top: -screenWidth * 0.45,
|
||||
left: -screenWidth * 0.2,
|
||||
right: -screenWidth * 0.2,
|
||||
child: Container(
|
||||
height: screenWidth * 1.1,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: RadialGradient(
|
||||
colors: [
|
||||
const Color(0xFF8ad5c9).withOpacity(0.4),
|
||||
const Color(0xFF8ad5c9).withOpacity(0.0),
|
||||
center: Alignment.center,
|
||||
radius: 0.5,
|
||||
colors: [const Color(0xFF8ad5c9).withOpacity(0.6), const Color(0xFF8ad5c9).withOpacity(0.0)],
|
||||
stops: const [0.2, 1.0],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Column(
|
||||
children: [
|
||||
// Área do Topo (Menu e Logo)
|
||||
SafeArea(
|
||||
child: Container(
|
||||
height: 100,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.menu_rounded, color: Color(0xFF57a7ed), size: 32),
|
||||
onPressed: () => _scaffoldKey.currentState?.openDrawer(),
|
||||
),
|
||||
Image.asset('assets/logo.png', height: 100, errorBuilder: (c,e,s) => const SizedBox(width: 100)),
|
||||
const SizedBox(width: 48), // Equilíbrio para o ícone do menu
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Image.asset(
|
||||
'assets/logo.png',
|
||||
height: 170,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
controller: _scrollController,
|
||||
padding: const EdgeInsets.symmetric(vertical: 20),
|
||||
reverse: true,
|
||||
itemCount: _messages.length + (_isTyping ? 1 : 0),
|
||||
itemBuilder: (context, index) {
|
||||
if (_isTyping && index == 0) return _buildTypingIndicator();
|
||||
int msgIndex = _isTyping ? index - 1 : index;
|
||||
return _buildMessage(_messages[msgIndex]);
|
||||
},
|
||||
),
|
||||
),
|
||||
_buildTextComposer(),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSidebar() {
|
||||
return Drawer(
|
||||
width: MediaQuery.of(context).size.width * 0.75,
|
||||
child: Column(
|
||||
children: [
|
||||
DrawerHeader(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(colors: [Color(0xFF8ad5c9), Color(0xFF57a7ed)]),
|
||||
),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.auto_awesome, color: Colors.white, size: 40),
|
||||
const SizedBox(height: 10),
|
||||
const Text('Histórico de Chats', style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.add_comment_rounded, color: Color(0xFF57a7ed)),
|
||||
title: const Text('Nova Conversa', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
onTap: _createNewSession,
|
||||
),
|
||||
const Divider(),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: _sessions.length,
|
||||
itemBuilder: (context, index) {
|
||||
final session = _sessions[index];
|
||||
final isSelected = _currentSession?.id == session.id;
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? const Color(0xFF8ad5c9).withOpacity(0.1) : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: ListTile(
|
||||
title: Text(session.title, maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.delete_outline_rounded, color: Colors.redAccent),
|
||||
onPressed: () => _deleteSession(session.id!),
|
||||
),
|
||||
onTap: () => _selectSession(session),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
144
lib/database_helper.dart
Normal file
@@ -0,0 +1,144 @@
|
||||
import 'package:sqflite/sqflite.dart';
|
||||
import 'package:path/path.dart';
|
||||
|
||||
class ChatSession {
|
||||
final int? id;
|
||||
final String title;
|
||||
final String timestamp;
|
||||
|
||||
ChatSession({this.id, required this.title, required this.timestamp});
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'id': id,
|
||||
'title': title,
|
||||
'timestamp': timestamp,
|
||||
};
|
||||
}
|
||||
|
||||
factory ChatSession.fromMap(Map<String, dynamic> map) {
|
||||
return ChatSession(
|
||||
id: map['id'],
|
||||
title: map['title'],
|
||||
timestamp: map['timestamp'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ChatMessage {
|
||||
final int? id;
|
||||
final int sessionId;
|
||||
final String text;
|
||||
final bool isAssistant;
|
||||
final String timestamp;
|
||||
|
||||
ChatMessage({
|
||||
this.id,
|
||||
required this.sessionId,
|
||||
required this.text,
|
||||
required this.isAssistant,
|
||||
required this.timestamp,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'id': id,
|
||||
'sessionId': sessionId,
|
||||
'text': text,
|
||||
'isAssistant': isAssistant ? 1 : 0,
|
||||
'timestamp': timestamp,
|
||||
};
|
||||
}
|
||||
|
||||
factory ChatMessage.fromMap(Map<String, dynamic> map) {
|
||||
return ChatMessage(
|
||||
id: map['id'],
|
||||
sessionId: map['sessionId'],
|
||||
text: map['text'],
|
||||
isAssistant: map['isAssistant'] == 1,
|
||||
timestamp: map['timestamp'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DatabaseHelper {
|
||||
static final DatabaseHelper instance = DatabaseHelper._init();
|
||||
static Database? _database;
|
||||
|
||||
DatabaseHelper._init();
|
||||
|
||||
Future<Database> get database async {
|
||||
if (_database != null) return _database!;
|
||||
_database = await _initDB('chat.db');
|
||||
return _database!;
|
||||
}
|
||||
|
||||
Future<Database> _initDB(String filePath) async {
|
||||
final dbPath = await getDatabasesPath();
|
||||
final path = join(dbPath, filePath);
|
||||
|
||||
return await openDatabase(
|
||||
path,
|
||||
version: 1,
|
||||
onCreate: _createDB,
|
||||
);
|
||||
}
|
||||
|
||||
Future _createDB(Database db, int version) async {
|
||||
await db.execute('''
|
||||
CREATE TABLE sessions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
title TEXT NOT NULL,
|
||||
timestamp TEXT NOT NULL
|
||||
)
|
||||
''');
|
||||
|
||||
await db.execute('''
|
||||
CREATE TABLE messages (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sessionId INTEGER NOT NULL,
|
||||
text TEXT NOT NULL,
|
||||
isAssistant INTEGER NOT NULL,
|
||||
timestamp TEXT NOT NULL,
|
||||
FOREIGN KEY (sessionId) REFERENCES sessions (id) ON DELETE CASCADE
|
||||
)
|
||||
''');
|
||||
}
|
||||
|
||||
// Session operations
|
||||
Future<int> createSession(String title) async {
|
||||
final db = await instance.database;
|
||||
return await db.insert('sessions', {
|
||||
'title': title,
|
||||
'timestamp': DateTime.now().toIso8601String(),
|
||||
});
|
||||
}
|
||||
|
||||
Future<List<ChatSession>> getSessions() async {
|
||||
final db = await instance.database;
|
||||
final result = await db.query('sessions', orderBy: 'timestamp DESC');
|
||||
return result.map((json) => ChatSession.fromMap(json)).toList();
|
||||
}
|
||||
|
||||
Future<void> deleteSession(int id) async {
|
||||
final db = await instance.database;
|
||||
await db.delete('sessions', where: 'id = ?', whereArgs: [id]);
|
||||
}
|
||||
|
||||
// Message operations
|
||||
Future<int> insertMessage(ChatMessage message) async {
|
||||
final db = await instance.database;
|
||||
return await db.insert('messages', message.toMap());
|
||||
}
|
||||
|
||||
Future<List<ChatMessage>> getMessages(int sessionId) async {
|
||||
final db = await instance.database;
|
||||
final result = await db.query(
|
||||
'messages',
|
||||
where: 'sessionId = ?',
|
||||
whereArgs: [sessionId],
|
||||
orderBy: 'timestamp ASC',
|
||||
);
|
||||
return result.map((json) => ChatMessage.fromMap(json)).toList();
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ class MyApp extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'EPVChat! Clone',
|
||||
title: 'EPVChat!',
|
||||
theme: ThemeData(
|
||||
brightness: Brightness.light,
|
||||
scaffoldBackgroundColor: Colors.white,
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
import FlutterMacOS
|
||||
import Foundation
|
||||
|
||||
import sqflite_darwin
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||
}
|
||||
|
||||
156
pubspec.lock
@@ -1,6 +1,14 @@
|
||||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
archive:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: archive
|
||||
sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.9"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -33,6 +41,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.1"
|
||||
checked_yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: checked_yaml
|
||||
sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.4"
|
||||
cli_util:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cli_util
|
||||
sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.4.2"
|
||||
clock:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -65,11 +89,27 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.3"
|
||||
ffi:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: ffi
|
||||
sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_launcher_icons:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: flutter_launcher_icons
|
||||
sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.13.1"
|
||||
flutter_lints:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
@@ -107,6 +147,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.2"
|
||||
image:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image
|
||||
sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.8.0"
|
||||
json_annotation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: json_annotation
|
||||
sha256: cb09e7dac6210041fad964ed7fbee004f14258b4eca4040f72d1234062ace4c8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.11.0"
|
||||
leak_tracker:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -172,13 +228,45 @@ packages:
|
||||
source: hosted
|
||||
version: "1.17.0"
|
||||
path:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path
|
||||
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.9.1"
|
||||
petitparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: petitparser
|
||||
sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.2"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: platform
|
||||
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.6"
|
||||
plugin_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: plugin_platform_interface
|
||||
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.8"
|
||||
posix:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: posix
|
||||
sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.5.0"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
@@ -192,6 +280,46 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.10.2"
|
||||
sqflite:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: sqflite
|
||||
sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.2"
|
||||
sqflite_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite_android
|
||||
sha256: "881e28efdcc9950fd8e9bb42713dcf1103e62a2e7168f23c9338d82db13dec40"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.2+3"
|
||||
sqflite_common:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite_common
|
||||
sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.6"
|
||||
sqflite_darwin:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite_darwin
|
||||
sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.2"
|
||||
sqflite_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite_platform_interface
|
||||
sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.0"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -216,6 +344,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.1"
|
||||
synchronized:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: synchronized
|
||||
sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.4.0"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -264,6 +400,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
xml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xml
|
||||
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.6.1"
|
||||
yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: yaml
|
||||
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
sdks:
|
||||
dart: ">=3.11.1 <4.0.0"
|
||||
flutter: ">=3.22.0"
|
||||
flutter: ">=3.38.0"
|
||||
|
||||
@@ -13,13 +13,22 @@ dependencies:
|
||||
cupertino_icons: ^1.0.8
|
||||
http: ^1.6.0
|
||||
flutter_markdown: ^0.7.3
|
||||
sqflite: ^2.3.0
|
||||
path: ^1.9.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
flutter_lints: ^6.0.0
|
||||
flutter_launcher_icons: ^0.13.1
|
||||
|
||||
flutter_icons:
|
||||
android: true
|
||||
ios: true
|
||||
image_path: "assets/icon/icon_logos.png"
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
assets:
|
||||
- assets/logo.png
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 33 KiB |