infria 0.1.2
infria: ^0.1.2 copied to clipboard
A clean, customizable Flutter UI SDK and runtime controller for AI chat interfaces, RAG, and dynamic client-side function calling.
example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:infria/infria.dart';
import 'infria_options.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Inisialisasi INFRIA SDK (Developer Experience setara FlutterFire)
await InfriaSDK.initializeApp(
options: DefaultInfriaOptions.currentPlatform,
);
runApp(const InfriaHealthcareShowcaseApp());
}
class InfriaHealthcareShowcaseApp extends StatefulWidget {
const InfriaHealthcareShowcaseApp({super.key});
@override
State<InfriaHealthcareShowcaseApp> createState() =>
_InfriaHealthcareShowcaseAppState();
}
class _InfriaHealthcareShowcaseAppState
extends State<InfriaHealthcareShowcaseApp> {
ThemeMode _themeMode = ThemeMode.light;
void _toggleTheme() {
setState(() {
_themeMode =
_themeMode == ThemeMode.light ? ThemeMode.dark : ThemeMode.light;
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'INFRIA — Healthcare AI Assistant Showcase',
debugShowCheckedModeBanner: false,
themeMode: _themeMode,
theme: ThemeData(
useMaterial3: true,
brightness: Brightness.light,
colorSchemeSeed: const Color(0xFF0284C7),
scaffoldBackgroundColor: const Color(0xFFF8FAFC),
),
darkTheme: ThemeData(
useMaterial3: true,
brightness: Brightness.dark,
colorSchemeSeed: const Color(0xFF38BDF8),
scaffoldBackgroundColor: const Color(0xFF0F172A),
),
home: MainHealthcareDashboard(
onToggleTheme: _toggleTheme,
isDark: _themeMode == ThemeMode.dark,
),
);
}
}
class MainHealthcareDashboard extends StatefulWidget {
final VoidCallback onToggleTheme;
final bool isDark;
const MainHealthcareDashboard({
super.key,
required this.onToggleTheme,
required this.isDark,
});
@override
State<MainHealthcareDashboard> createState() =>
_MainHealthcareDashboardState();
}
class _MainHealthcareDashboardState extends State<MainHealthcareDashboard> {
int _currentIndex = 0;
late final ChatController _controller;
// Pre-configured Healthcare Quick Suggestions for Inclusive UX
static const List<QuickSuggestion> _healthcareSuggestions = [
QuickSuggestion(
label: '🩺 Cek Antrean Poli',
message: 'Berapa nomor antrean saya di Poli Gigi sekarang?',
icon: Icons.access_time_rounded,
),
QuickSuggestion(
label: '📅 Jadwal Dokter',
message: 'Tampilkan jadwal dokter spesialis anak hari ini',
icon: Icons.calendar_today_rounded,
),
QuickSuggestion(
label: '📋 Syarat Rujukan BPJS',
message: 'Apa saja dokumen persyaratan untuk berobat menggunakan BPJS?',
icon: Icons.description_rounded,
),
QuickSuggestion(
label: '❌ Batalkan Janji Temu',
message: 'Tolong batalkan janji temu dokter saya dengan ID APPT-891',
icon: Icons.cancel_outlined,
),
];
@override
void initState() {
super.initState();
// 1. Registrasi Dynamic Client-Side Function Callbacks via InfriaSDK (DX Ringkas)
_registerHealthcareFunctions();
// 2. Inisialisasi ChatController (Auto-binds ke InfriaSDK.instance secara global)
_controller = ChatController(
initialMessages: [
ChatMessage.assistant(
id: 'welcome_1',
content:
'Halo! Selamat datang di **Asisten Layanan Kesehatan Puskesmas Sehat**.\n\nSaya dapat membantu Anda mengecek nomor antrean poli secara langsung, memeriksa jadwal dokter, hingga syarat rujukan BPJS.',
),
],
onError: (error) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Kendala komunikasi: $error'),
backgroundColor: Colors.red.shade700,
),
);
}
},
);
}
void _registerHealthcareFunctions() {
// Fungsi 1: Cek Antrean Poli Real-time
InfriaSDK.onFunctionCall(
'check_queue_status',
(args) async {
await Future<void>.delayed(const Duration(milliseconds: 900));
final poli = args['poliId']?.toString() ?? 'Poli Gigi';
return {
'poli': poli,
'nomor_antrean_anda': 'A-15',
'sedang_dilayani': 'A-13',
'sisa_antrean': 2,
'estimasi_menit': 10,
};
},
description: 'Mengecek nomor antrean pasien di sistem poli kesehatan',
parameters: const [
FunctionParameter.string('poliId', description: 'Nama atau ID poli'),
],
);
// Fungsi 2: Cek Jadwal Dokter
InfriaSDK.onFunctionCall(
'get_doctor_schedule',
(args) async {
await Future<void>.delayed(const Duration(milliseconds: 700));
return {
'dokter': 'dr. Amanda Putri, Sp.A',
'spesialisasi': 'Spesialis Anak',
'jadwal': 'Senin & Rabu (09:00 - 12:00 WIB)',
'kuota_tersedia': 8,
'ruangan': 'Poli Anak Lantai 2',
};
},
description: 'Mengambil jadwal praktek dokter dari SIMRS',
);
// Fungsi 3: Cek Status Rujukan BPJS
InfriaSDK.onFunctionCall(
'get_bpjs_referral_status',
(args) async {
await Future<void>.delayed(const Duration(milliseconds: 800));
return {
'status': 'AKTIF',
'faskes_asal': 'Puskesmas Sukamaju',
'faskes_tujuan': 'RSUD Sehat Sentosa',
'masa_berlaku_sampai': '30 September 2026',
};
},
description: 'Mengecek validitas surat rujukan BPJS Kesehatan',
parameters: const [
FunctionParameter.string('patientId', description: 'Nomor Kartu BPJS / NIK Pasien'),
],
);
// Fungsi 4: Batalkan Janji Temu (Memerlukan Konfirmasi Pengguna)
InfriaSDK.onFunctionCall(
'cancel_appointment',
(args) async {
final confirmed = await showFunctionConfirmationDialog(
context: context,
functionName: 'cancel_appointment',
description:
'Membatalkan janji temu dokter untuk ID: ${args['appointmentId'] ?? 'APPT-891'}',
);
if (!confirmed) {
return {'status': 'DIBATALKAN_OLEH_PENGGUNA'};
}
await Future<void>.delayed(const Duration(milliseconds: 600));
return {
'status': 'BERHASIL_DIBATALKAN',
'id': args['appointmentId'] ?? 'APPT-891',
};
},
description: 'Membatalkan janji temu dokter yang sudah terjadwal',
parameters: const [
FunctionParameter.string('appointmentId', description: 'ID Janji Temu (Booking Code)', isRequired: true),
],
requiresConfirmation: true,
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Row(
children: [
Icon(Icons.local_hospital_rounded,
color: Color(0xFF0284C7), size: 20),
SizedBox(width: 8),
Text(
'Puskesmas Sehat AI',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
),
],
),
actions: [
IconButton(
tooltip: 'Ganti Tema',
icon: Icon(widget.isDark
? Icons.light_mode_rounded
: Icons.dark_mode_rounded),
onPressed: widget.onToggleTheme,
),
IconButton(
tooltip: 'Hapus Percakapan',
icon: const Icon(Icons.delete_outline_rounded),
onPressed: () {
_controller.clear(newSession: true);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Sesi percakapan baru dimulai'),
duration: Duration(seconds: 1),
),
);
},
),
],
),
body: IndexedStack(
index: _currentIndex,
children: [
// 1. Dashboard View (with Floating Button & Presentation Modals)
_buildDashboardView(context),
// 2. Fullscreen Inclusive Chat View with Quick Suggestions & Feedback
InfriaChat(
controller: _controller,
hintText: 'Tanyakan antrean, jadwal dokter, BPJS...',
suggestions: _healthcareSuggestions,
enableFeedback: true,
onFeedback: (msg, rating) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Feedback ${rating.value} tercatat!'),
duration: const Duration(seconds: 1),
),
);
},
),
// 3. Custom Builder Chat View
_buildCustomBuilderView(context),
],
),
floatingActionButton: _currentIndex == 0
? InfriaChatFloatingButton(
controller: _controller,
tooltip: 'Tanya Asisten AI (Bottom Sheet)',
suggestions: _healthcareSuggestions,
enableFeedback: true,
)
: null,
bottomNavigationBar: NavigationBar(
selectedIndex: _currentIndex,
onDestinationSelected: (index) {
setState(() {
_currentIndex = index;
});
},
destinations: const [
NavigationDestination(
icon: Icon(Icons.dashboard_outlined),
selectedIcon: Icon(Icons.dashboard_rounded),
label: 'Dashboard (FAB)',
),
NavigationDestination(
icon: Icon(Icons.forum_outlined),
selectedIcon: Icon(Icons.forum_rounded),
label: 'Asisten Penuh',
),
NavigationDestination(
icon: Icon(Icons.palette_outlined),
selectedIcon: Icon(Icons.palette_rounded),
label: 'Custom Builder',
),
],
),
);
}
Widget _buildDashboardView(BuildContext context) {
final isDark = widget.isDark;
return SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header Card
Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: isDark
? [const Color(0xFF0F172A), const Color(0xFF1E293B)]
: [const Color(0xFF0284C7), const Color(0xFF0369A1)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: (isDark ? Colors.black : const Color(0xFF0284C7))
.withAlpha(50),
blurRadius: 12,
offset: const Offset(0, 4),
),
],
),
child: const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Layanan Kesehatan Digital',
style: TextStyle(
color: Colors.white,
fontSize: 22,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 8),
Text(
'Asisten AI terintegrasi dengan Dynamic Function Calling untuk pengecekan antrean poli, jadwal dokter, dan status BPJS secara inklusif.',
style: TextStyle(color: Colors.white70, fontSize: 14),
),
],
),
),
const SizedBox(height: 24),
// Stat Cards Row
Row(
children: [
Expanded(
child: _buildStatCard(
title: 'Fungsi Terdaftar',
value: '${InfriaSDK.instance.functions.count} Layanan',
icon: Icons.electrical_services_rounded,
color: Colors.teal,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildStatCard(
title: 'Status AI',
value: _controller.isBusy ? 'Memproses...' : 'Siap',
icon: Icons.check_circle_rounded,
color: _controller.isBusy ? Colors.amber : Colors.green,
),
),
],
),
const SizedBox(height: 28),
const Text(
'Uji Coba Tampilan Modal (Presentation Modes)',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 14),
// Action Tiles
Card(
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: BorderSide(
color: isDark ? Colors.white12 : Colors.black12,
),
),
child: Column(
children: [
ListTile(
leading: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.blue.withAlpha(30),
shape: BoxShape.circle,
),
child: const Icon(Icons.call_to_action_rounded,
color: Colors.blue),
),
title: const Text('Buka Chat via Bottom Sheet'),
subtitle: const Text('Tampilan slide-up dari bawah layar'),
trailing: const Icon(Icons.arrow_forward_ios_rounded, size: 16),
onTap: () {
showInfriaChatBottomSheet(
context: context,
controller: _controller,
suggestions: _healthcareSuggestions,
enableFeedback: true,
);
},
),
const Divider(height: 1),
ListTile(
leading: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.teal.withAlpha(30),
shape: BoxShape.circle,
),
child: const Icon(Icons.open_in_browser_rounded,
color: Colors.teal),
),
title: const Text('Buka Chat via Dialog Popup'),
subtitle: const Text('Tampilan modal window di tengah layar'),
trailing: const Icon(Icons.arrow_forward_ios_rounded, size: 16),
onTap: () {
showInfriaChatDialog(
context: context,
controller: _controller,
suggestions: _healthcareSuggestions,
enableFeedback: true,
);
},
),
],
),
),
],
),
);
}
Widget _buildStatCard({
required String title,
required String value,
required IconData icon,
required Color color,
}) {
final isDark = widget.isDark;
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: isDark ? const Color(0xFF1E293B) : Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: isDark ? Colors.white10 : Colors.black12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
title,
style: TextStyle(
color: isDark ? Colors.white70 : Colors.black54,
fontSize: 13,
),
),
Icon(icon, color: color, size: 20),
],
),
const SizedBox(height: 10),
Text(
value,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
],
),
);
}
Widget _buildCustomBuilderView(BuildContext context) {
return InfriaChat(
controller: _controller,
headerBuilder: (context, controller) {
return Container(
padding: const EdgeInsets.all(12),
color: Colors.teal.withAlpha(30),
child: Row(
children: [
const Icon(Icons.medical_services_rounded, color: Colors.teal),
const SizedBox(width: 8),
const Text(
'Kustom UI — Tema Rumah Sakit',
style: TextStyle(
fontWeight: FontWeight.bold, color: Colors.teal),
),
const Spacer(),
Text(
'${controller.messageCount} pesan',
style: const TextStyle(fontSize: 12, color: Colors.teal),
),
],
),
);
},
messageBubbleBuilder: (context, message) {
final isUser = message.isUser;
return Container(
margin: const EdgeInsets.symmetric(vertical: 6, horizontal: 12),
alignment: isUser ? Alignment.centerRight : Alignment.centerLeft,
child: Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: isUser ? const Color(0xFF0284C7) : const Color(0xFF0F766E),
borderRadius: BorderRadius.circular(16),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
isUser ? Icons.person : Icons.health_and_safety_rounded,
color: Colors.white,
size: 16,
),
const SizedBox(width: 8),
Flexible(
child: Text(
message.content,
style: const TextStyle(color: Colors.white, fontSize: 14),
),
),
],
),
),
);
},
);
}
}