forked from tonycho/Awesome-Agentic-AI
Updates for 'mobile-flutter' libraries with 'Model Selector UI'
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -321,3 +321,6 @@ vite.config.ts.timestamp-*
|
||||
.cursor/rules/todo2.mdc
|
||||
.cursor/mcp.json
|
||||
.todo2/
|
||||
|
||||
# Exclude 'mobile-flutter/lib'
|
||||
!mobile-flutter/lib
|
||||
|
||||
59
mobile-flutter/lib/main.dart
Normal file
59
mobile-flutter/lib/main.dart
Normal file
@@ -0,0 +1,59 @@
|
||||
// lib/main.dart
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'screens/memory_timeline_screen.dart';
|
||||
import 'screens/agent_status_screen.dart'; // 🔜 coming next
|
||||
|
||||
void main() {
|
||||
runApp(const AgentSyncApp());
|
||||
}
|
||||
|
||||
class AgentSyncApp extends StatelessWidget {
|
||||
const AgentSyncApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'Agent Sync',
|
||||
theme: ThemeData(
|
||||
primarySwatch: Colors.indigo,
|
||||
scaffoldBackgroundColor: Colors.grey[100],
|
||||
visualDensity: VisualDensity.adaptivePlatformDensity,
|
||||
),
|
||||
initialRoute: '/',
|
||||
routes: {
|
||||
// '/': (context) => const HomeScreen(),
|
||||
'/': (context) => const DashboardScreen(),
|
||||
'/memory': (context) => const MemoryTimelineScreen(tenantId: "default", agentRole: "planner"),
|
||||
'/status': (context) => const AgentStatusScreen(tenantId: "default", agentRole: "planner"), // 🔜
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class HomeScreen extends StatelessWidget {
|
||||
const HomeScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text("🧭 Agent Control Panel")),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.pushNamed(context, '/memory'),
|
||||
child: const Text("🧠 View Memory Timeline"),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.pushNamed(context, '/status'),
|
||||
child: const Text("📡 View Agent Status"),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
71
mobile-flutter/lib/screens/agent_status_screen.dart
Normal file
71
mobile-flutter/lib/screens/agent_status_screen.dart
Normal file
@@ -0,0 +1,71 @@
|
||||
// lib/screens/agent_status_screen.dart
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import '../services/sync_service.dart';
|
||||
|
||||
class AgentStatusScreen extends StatefulWidget {
|
||||
final String tenantId;
|
||||
final String agentRole;
|
||||
|
||||
const AgentStatusScreen({super.key, required this.tenantId, required this.agentRole});
|
||||
|
||||
@override
|
||||
State<AgentStatusScreen> createState() => _AgentStatusScreenState();
|
||||
}
|
||||
|
||||
class _AgentStatusScreenState extends State<AgentStatusScreen> {
|
||||
Map<String, dynamic>? state;
|
||||
Map<String, dynamic>? status;
|
||||
bool loading = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
fetchStatus();
|
||||
}
|
||||
|
||||
Future<void> fetchStatus() async {
|
||||
final agentState = await SyncService.getAgentState(widget.tenantId, widget.agentRole);
|
||||
final agentStatus = await SyncService.getAgentStatus(widget.tenantId, widget.agentRole);
|
||||
setState(() {
|
||||
state = agentState;
|
||||
status = agentStatus;
|
||||
loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
Widget buildJsonBlock(String title, Map<String, dynamic>? data) {
|
||||
if (data == null) return const SizedBox.shrink();
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title, style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 6),
|
||||
Text(data.toString(), style: const TextStyle(fontSize: 12)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text("📡 Agent Status")),
|
||||
body: loading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: ListView(
|
||||
children: [
|
||||
buildJsonBlock("🧠 Agent State", state),
|
||||
buildJsonBlock("📍 Agent Status", status),
|
||||
if (status?["recent"] != null)
|
||||
buildJsonBlock("🕒 Recent Activity", Map<String, dynamic>.from(status!["recent"])),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
103
mobile-flutter/lib/screens/cluster_summaries_screen.dart
Normal file
103
mobile-flutter/lib/screens/cluster_summaries_screen.dart
Normal file
@@ -0,0 +1,103 @@
|
||||
// lib/screens/cluster_summaries_screen.dart
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import '../services/sync_service.dart';
|
||||
|
||||
class ClusterSummariesScreen extends StatefulWidget {
|
||||
final String tenantId;
|
||||
final String agentRole;
|
||||
|
||||
const ClusterSummariesScreen({super.key, required this.tenantId, required this.agentRole});
|
||||
|
||||
@override
|
||||
State<ClusterSummariesScreen> createState() => _ClusterSummariesScreenState();
|
||||
}
|
||||
|
||||
class _ClusterSummariesScreenState extends State<ClusterSummariesScreen> {
|
||||
Map<String, dynamic> summaries = {};
|
||||
bool loading = true;
|
||||
String? error;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fetchSummaries();
|
||||
}
|
||||
|
||||
Future<void> _fetchSummaries() async {
|
||||
setState(() {
|
||||
loading = true;
|
||||
error = null;
|
||||
});
|
||||
try {
|
||||
final data = await SyncService.getClusterSummaries(widget.tenantId, widget.agentRole);
|
||||
setState(() {
|
||||
summaries = data;
|
||||
loading = false;
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
error = e.toString();
|
||||
loading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final keys = summaries.keys.toList()..sort();
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text("🧩 Cluster Summaries"),
|
||||
actions: [
|
||||
IconButton(icon: const Icon(Icons.refresh), onPressed: _fetchSummaries),
|
||||
],
|
||||
),
|
||||
body: loading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: error != null
|
||||
? Center(child: Text("Error: $error"))
|
||||
: keys.isEmpty
|
||||
? const Center(child: Text("No clusters available yet."))
|
||||
: ListView.builder(
|
||||
itemCount: keys.length,
|
||||
itemBuilder: (context, index) {
|
||||
final key = keys[index];
|
||||
final item = summaries[key] as Map<String, dynamic>;
|
||||
final summary = item['summary']?.toString() ?? '';
|
||||
final queries = (item['queries'] as List<dynamic>? ?? []).cast<String>();
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text("Cluster $key", style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 6),
|
||||
Text(summary),
|
||||
const SizedBox(height: 8),
|
||||
const Text("Representative queries:", style: TextStyle(fontWeight: FontWeight.w500)),
|
||||
const SizedBox(height: 4),
|
||||
for (final q in queries.take(5))
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text("• "),
|
||||
Expanded(child: Text(q)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
113
mobile-flutter/lib/screens/dashboard_screen.dart
Normal file
113
mobile-flutter/lib/screens/dashboard_screen.dart
Normal file
@@ -0,0 +1,113 @@
|
||||
// lib/screens/dashboard_screen.dart
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'memory_timeline_screen.dart';
|
||||
import 'agent_status_screen.dart';
|
||||
import 'voice_input_screen.dart';
|
||||
|
||||
class DashboardScreen extends StatelessWidget {
|
||||
final String tenantId = "default";
|
||||
final String agentRole = "planner";
|
||||
|
||||
const DashboardScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text("🧭 Agentic Dashboard")),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
ElevatedButton.icon(
|
||||
icon: const Icon(Icons.record_voice_over),
|
||||
label: const Text("🎙️ Voice Recorder"),
|
||||
onPressed: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const VoiceInputScreen()),
|
||||
),
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
icon: const Icon(Icons.memory),
|
||||
label: const Text("🧠 Memory Timeline"),
|
||||
onPressed: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => MemoryTimelineScreen(tenantId: tenantId, agentRole: agentRole),
|
||||
),
|
||||
),
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
icon: const Icon(Icons.scatter_plot),
|
||||
label: const Text("📊 Topic Graph"),
|
||||
onPressed: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => TopicGraphScreen(tenantId: tenantId, agentRole: agentRole),
|
||||
),
|
||||
),
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
icon: const Icon(Icons.local_fire_department),
|
||||
label: const Text("🔥 Heatmap"),
|
||||
onPressed: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => HeatmapScreen(tenantId: tenantId, agentRole: agentRole),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
ElevatedButton.icon(
|
||||
icon: const Icon(Icons.view_module),
|
||||
label: const Text("🧩 Cluster Summaries"),
|
||||
onPressed: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => ClusterSummariesScreen(tenantId: tenantId, agentRole: agentRole),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton.icon(
|
||||
icon: const Icon(Icons.wifi),
|
||||
label: const Text("📡 Agent Status"),
|
||||
onPressed: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => AgentStatusScreen(tenantId: tenantId, agentRole: agentRole),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton.icon(
|
||||
icon: const Icon(Icons.mic),
|
||||
label: const Text("🎙️ Voice Input"),
|
||||
onPressed: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => VoiceInputScreen(tenantId: tenantId, agentRole: agentRole),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton.icon(
|
||||
icon: const Icon(Icons.settings_suggest),
|
||||
label: const Text("🤖 Model Selector"),
|
||||
onPressed: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const ModelSelectorScreen()),
|
||||
),
|
||||
),
|
||||
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
179
mobile-flutter/lib/screens/heatmap_screen.dart
Normal file
179
mobile-flutter/lib/screens/heatmap_screen.dart
Normal file
@@ -0,0 +1,179 @@
|
||||
// lib/screens/heatmap_screen.dart
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import '../services/sync_service.dart';
|
||||
|
||||
class HeatmapScreen extends StatefulWidget {
|
||||
final String tenantId;
|
||||
final String agentRole;
|
||||
|
||||
const HeatmapScreen({super.key, required this.tenantId, required this.agentRole});
|
||||
|
||||
@override
|
||||
State<HeatmapScreen> createState() => _HeatmapScreenState();
|
||||
}
|
||||
|
||||
class _HeatmapScreenState extends State<HeatmapScreen> {
|
||||
List<Map<String, dynamic>> buckets = [];
|
||||
bool loading = true;
|
||||
String? error;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fetchHeatmap();
|
||||
}
|
||||
|
||||
Future<void> _fetchHeatmap() async {
|
||||
setState(() {
|
||||
loading = true;
|
||||
error = null;
|
||||
});
|
||||
try {
|
||||
final data = await SyncService.getHeatmap(widget.tenantId, widget.agentRole);
|
||||
setState(() {
|
||||
buckets = data;
|
||||
loading = false;
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
error = e.toString();
|
||||
loading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize counts to color intensity
|
||||
Color _colorForValue(num value, num min, num max) {
|
||||
if (max <= min) return Colors.grey.shade200;
|
||||
final t = ((value - min) / (max - min)).clamp(0, 1).toDouble();
|
||||
// Indigo scale: light to deep
|
||||
return Color.lerp(Colors.indigo.shade100, Colors.indigo.shade700, t)!;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final minVal = buckets.isEmpty ? 0 : (buckets.map((b) => (b['count'] as num)).reduce((a, b) => a < b ? a : b));
|
||||
final maxVal = buckets.isEmpty ? 0 : (buckets.map((b) => (b['count'] as num)).reduce((a, b) => a > b ? a : b));
|
||||
final topics = buckets.map((b) => b['topic'] as String).toSet().toList();
|
||||
final times = buckets.map((b) => b['time_bucket'] as String).toSet().toList();
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text("🔥 Memory Heatmap"),
|
||||
actions: [
|
||||
IconButton(icon: const Icon(Icons.refresh), onPressed: _fetchHeatmap),
|
||||
],
|
||||
),
|
||||
body: loading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: error != null
|
||||
? Center(child: Text("Error: $error"))
|
||||
: buckets.isEmpty
|
||||
? const Center(child: Text("No heatmap data yet."))
|
||||
: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text("Intensity by topic and time", style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 8),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Topic labels
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
const SizedBox(height: 40),
|
||||
for (final topic in topics)
|
||||
Container(
|
||||
width: 120,
|
||||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 8),
|
||||
child: Text(topic, textAlign: TextAlign.right),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// Heatmap grid
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Column(
|
||||
children: [
|
||||
// Time headers
|
||||
Row(
|
||||
children: [
|
||||
for (final t in times)
|
||||
Container(
|
||||
width: 80,
|
||||
height: 40,
|
||||
alignment: Alignment.center,
|
||||
child: Text(t, style: const TextStyle(fontWeight: FontWeight.w500)),
|
||||
),
|
||||
],
|
||||
),
|
||||
// Grid cells
|
||||
for (final topic in topics)
|
||||
Row(
|
||||
children: [
|
||||
for (final t in times)
|
||||
Builder(
|
||||
builder: (_) {
|
||||
final cell = buckets.firstWhere(
|
||||
(b) => b['topic'] == topic && b['time_bucket'] == t,
|
||||
orElse: () => {'count': 0},
|
||||
);
|
||||
final count = (cell['count'] as num);
|
||||
return Container(
|
||||
width: 80,
|
||||
height: 40,
|
||||
margin: const EdgeInsets.all(2),
|
||||
decoration: BoxDecoration(
|
||||
color: _colorForValue(count, minVal, maxVal),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
count.toInt().toString(),
|
||||
style: const TextStyle(color: Colors.white, fontSize: 12),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: const [
|
||||
Text("Low"),
|
||||
SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: LinearProgressIndicator(
|
||||
value: 1,
|
||||
minHeight: 8,
|
||||
color: Color(0xFF303F9F),
|
||||
backgroundColor: Color(0xFFC5CAE9),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Text("High"),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
160
mobile-flutter/lib/screens/memory_timeline_screen.dart
Normal file
160
mobile-flutter/lib/screens/memory_timeline_screen.dart
Normal file
@@ -0,0 +1,160 @@
|
||||
// lib/screens/memory_timeline_screen.dart
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import '../services/sync_service.dart';
|
||||
import '../widgets/emotion_badge.dart';
|
||||
import '../widgets/emotion_intensity_bar.dart';
|
||||
|
||||
class MemoryTimelineScreen extends StatefulWidget {
|
||||
final String tenantId;
|
||||
final String agentRole;
|
||||
|
||||
const MemoryTimelineScreen({super.key, required this.tenantId, required this.agentRole});
|
||||
|
||||
@override
|
||||
State<MemoryTimelineScreen> createState() => _MemoryTimelineScreenState();
|
||||
}
|
||||
|
||||
class _MemoryTimelineScreenState extends State<MemoryTimelineScreen> {
|
||||
List<Map<String, dynamic>> episodes = [];
|
||||
bool loading = true;
|
||||
String? error;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
fetchMemory();
|
||||
}
|
||||
|
||||
// Future<void> fetchMemory() async {
|
||||
// final data = await SyncService.getAgentMemory(widget.tenantId, widget.agentRole);
|
||||
// setState(() {
|
||||
// episodes = data;
|
||||
// loading = false;
|
||||
// });
|
||||
// }
|
||||
Future<void> fetchMemory() async {
|
||||
try {
|
||||
final data = await SyncService.listAgentMemory(widget.tenantId, widget.agentRole);
|
||||
setState(() {
|
||||
episodes = data;
|
||||
loading = false;
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
error = e.toString();
|
||||
loading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> searchMemory(String query) async {
|
||||
setState(() => loading = true);
|
||||
try {
|
||||
final data = await SyncService.searchAgentMemory(widget.tenantId, widget.agentRole, query);
|
||||
setState(() {
|
||||
episodes = data;
|
||||
loading = false;
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
error = e.toString();
|
||||
loading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
// appBar: AppBar(title: const Text("🧠 Memory Timeline")),
|
||||
appBar: AppBar(
|
||||
title: const Text("🧠 Memory Timeline"),
|
||||
actions: [
|
||||
IconButton(icon: const Icon(Icons.refresh), onPressed: fetchMemory),
|
||||
],
|
||||
),
|
||||
body: loading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
// : ListView.builder(
|
||||
// itemCount: episodes.length,
|
||||
// itemBuilder: (context, index) {
|
||||
// final episode = episodes[index];
|
||||
// return Card(
|
||||
// margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
// child: Padding(
|
||||
// padding: const EdgeInsets.all(12),
|
||||
// child: Column(
|
||||
// crossAxisAlignment: CrossAxisAlignment.start,
|
||||
// children: [
|
||||
// Text("Task: ${episode['task']}", style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
// if (episode.containsKey('score'))
|
||||
// Text("Score: ${episode['score']}", style: const TextStyle(color: Colors.blue)),
|
||||
// const SizedBox(height: 6),
|
||||
// Text("Output:", style: const TextStyle(fontWeight: FontWeight.w500)),
|
||||
// Text(episode['output'].toString(), style: const TextStyle(fontSize: 12)),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// },
|
||||
// ),
|
||||
: error != null
|
||||
? Center(child: Text("Error: $error"))
|
||||
: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: TextField(
|
||||
decoration: const InputDecoration(
|
||||
labelText: "Search memories",
|
||||
prefixIcon: Icon(Icons.search),
|
||||
),
|
||||
onSubmitted: searchMemory,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: episodes.length,
|
||||
itemBuilder: (context, index) {
|
||||
final episode = episodes[index];
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text("Task: ${episode['task']}", style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
if (episode.containsKey('score'))
|
||||
Text("Score: ${episode['score']}", style: const TextStyle(color: Colors.blue)),
|
||||
const SizedBox(height: 6),
|
||||
Text("Output:", style: const TextStyle(fontWeight: FontWeight.w500)),
|
||||
Text(episode['output'].toString(), style: const TextStyle(fontSize: 12)),
|
||||
const SizedBox(height: 8),
|
||||
if (episode.containsKey('emotion')) ...[
|
||||
EmotionBadge(
|
||||
label: episode['emotion']['label'],
|
||||
score: (episode['emotion']['score'] as num).toDouble(),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
EmotionIntensityBar(
|
||||
label: episode['emotion']['label'],
|
||||
score: (episode['emotion']['score'] as num).toDouble(),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 8),
|
||||
Text("⏰ ${episode['timestamp']}", style: const TextStyle(fontSize: 12, color: Colors.grey)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
);
|
||||
}
|
||||
}
|
||||
99
mobile-flutter/lib/screens/model_selector_screen.dart
Normal file
99
mobile-flutter/lib/screens/model_selector_screen.dart
Normal file
@@ -0,0 +1,99 @@
|
||||
// lib/screens/model_selector_screen.dart
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import '../services/model_service.dart';
|
||||
|
||||
class ModelSelectorScreen extends StatefulWidget {
|
||||
const ModelSelectorScreen({super.key});
|
||||
|
||||
@override
|
||||
State<ModelSelectorScreen> createState() => _ModelSelectorScreenState();
|
||||
}
|
||||
|
||||
class _ModelSelectorScreenState extends State<ModelSelectorScreen> {
|
||||
final _controller = TextEditingController();
|
||||
String selectedModel = "llm";
|
||||
Map<String, dynamic>? result;
|
||||
bool loading = false;
|
||||
String? error;
|
||||
|
||||
final modelTypes = [
|
||||
"llm",
|
||||
"slm",
|
||||
"embed",
|
||||
"vlm",
|
||||
"moe",
|
||||
"lcm",
|
||||
"lam",
|
||||
"mlm",
|
||||
];
|
||||
|
||||
Future<void> _runModel() async {
|
||||
setState(() {
|
||||
loading = true;
|
||||
error = null;
|
||||
result = null;
|
||||
});
|
||||
try {
|
||||
final data = await ModelService.queryModel(selectedModel, _controller.text);
|
||||
setState(() {
|
||||
result = data;
|
||||
loading = false;
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
error = e.toString();
|
||||
loading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text("🤖 Model Selector")),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
DropdownButtonFormField<String>(
|
||||
value: selectedModel,
|
||||
items: modelTypes
|
||||
.map((m) => DropdownMenuItem(value: m, child: Text(m.toUpperCase())))
|
||||
.toList(),
|
||||
onChanged: (val) => setState(() => selectedModel = val!),
|
||||
decoration: const InputDecoration(labelText: "Select Model Type"),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: _controller,
|
||||
decoration: const InputDecoration(
|
||||
labelText: "Enter prompt or text",
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
maxLines: 3,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton.icon(
|
||||
icon: const Icon(Icons.play_arrow),
|
||||
label: const Text("Run Model"),
|
||||
onPressed: _runModel,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
if (loading) const CircularProgressIndicator(),
|
||||
if (error != null) Text("Error: $error", style: const TextStyle(color: Colors.red)),
|
||||
if (result != null)
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Text(
|
||||
result.toString(),
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
94
mobile-flutter/lib/screens/topic_graph_screen.dart
Normal file
94
mobile-flutter/lib/screens/topic_graph_screen.dart
Normal file
@@ -0,0 +1,94 @@
|
||||
// lib/screens/topic_graph_screen.dart
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:graphview/GraphView.dart';
|
||||
import '../services/sync_service.dart';
|
||||
|
||||
class TopicGraphScreen extends StatefulWidget {
|
||||
final String tenantId;
|
||||
final String agentRole;
|
||||
|
||||
const TopicGraphScreen({super.key, required this.tenantId, required this.agentRole});
|
||||
|
||||
@override
|
||||
State<TopicGraphScreen> createState() => _TopicGraphScreenState();
|
||||
}
|
||||
|
||||
class _TopicGraphScreenState extends State<TopicGraphScreen> {
|
||||
Graph graph = Graph();
|
||||
bool loading = true;
|
||||
String? error;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
fetchGraph();
|
||||
}
|
||||
|
||||
Future<void> fetchGraph() async {
|
||||
try {
|
||||
final data = await SyncService.getTopicGraph(widget.tenantId, widget.agentRole);
|
||||
final nodes = data["nodes"] as List<dynamic>;
|
||||
final edges = data["edges"] as List<dynamic>;
|
||||
|
||||
final nodeMap = <String, Node>{};
|
||||
|
||||
for (var n in nodes) {
|
||||
final id = n["id"].toString();
|
||||
final label = n["label"];
|
||||
final node = Node.Id(id);
|
||||
nodeMap[id] = node;
|
||||
graph.addNode(node);
|
||||
}
|
||||
|
||||
for (var e in edges) {
|
||||
final from = e["from"].toString();
|
||||
final to = e["to"].toString();
|
||||
if (nodeMap.containsKey(from) && nodeMap.containsKey(to)) {
|
||||
graph.addEdge(nodeMap[from]!, nodeMap[to]!);
|
||||
}
|
||||
}
|
||||
|
||||
setState(() => loading = false);
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
error = e.toString();
|
||||
loading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final builder = FruchtermanReingoldAlgorithm(iterations: 100);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text("📊 Topic Graph")),
|
||||
body: loading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: error != null
|
||||
? Center(child: Text("Error: $error"))
|
||||
: InteractiveViewer(
|
||||
constrained: false,
|
||||
boundaryMargin: const EdgeInsets.all(100),
|
||||
minScale: 0.01,
|
||||
maxScale: 5.0,
|
||||
child: GraphView(
|
||||
graph: graph,
|
||||
algorithm: builder,
|
||||
builder: (Node node) {
|
||||
// node.id is the string id
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.indigo.shade100,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(node.key!.value.toString()),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
164
mobile-flutter/lib/screens/voice_input_screen.dart
Normal file
164
mobile-flutter/lib/screens/voice_input_screen.dart
Normal file
@@ -0,0 +1,164 @@
|
||||
// lib/screens/voice_input_screen.dart
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:speech_to_text/speech_to_text.dart' as stt;
|
||||
import '../services/sync_service.dart';
|
||||
import '../widgets/emotion_badge.dart';
|
||||
import '../widgets/emotion_intensity_bar.dart';
|
||||
import '../widgets/waveform_player.dart';
|
||||
import 'voice_playback_screen.dart';
|
||||
|
||||
class VoiceInputScreen extends StatefulWidget {
|
||||
final String tenantId;
|
||||
final String agentRole;
|
||||
|
||||
const VoiceInputScreen({super.key, required this.tenantId, required this.agentRole});
|
||||
|
||||
@override
|
||||
State<VoiceInputScreen> createState() => _VoiceInputScreenState();
|
||||
}
|
||||
|
||||
class _VoiceInputScreenState extends State<VoiceInputScreen> {
|
||||
late stt.SpeechToText _speech;
|
||||
bool _isListening = false;
|
||||
String _transcript = "";
|
||||
String? _audioPath;
|
||||
Map<String, dynamic>? _response;
|
||||
Map<String, dynamic>? _emotion;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_speech = stt.SpeechToText();
|
||||
}
|
||||
|
||||
Future<void> _startListening() async {
|
||||
bool available = await _speech.initialize();
|
||||
if (available) {
|
||||
setState(() => _isListening = true);
|
||||
_speech.listen(
|
||||
onResult: (result) => setState(() => _transcript = result.recognizedWords),
|
||||
localeId: "ko_KR",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _stopListening() {
|
||||
_speech.stop();
|
||||
setState(() => _isListening = false);
|
||||
}
|
||||
|
||||
Future<void> _sendTranscript() async {
|
||||
final res = await SyncService.sendVoiceTranscript(widget.tenantId, widget.agentRole, _transcript);
|
||||
setState(() {
|
||||
_response = res["response"];
|
||||
_emotion = res["emotion"];
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _transcribeAndSend(String audioPath) async {
|
||||
// TODO: Upload audio to backend and get transcript
|
||||
// For now, simulate transcript
|
||||
setState(() => _transcript = "Simulated transcript from audio");
|
||||
|
||||
await _sendTranscript(); // reuse existing method
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text("🎙️ Voice Input")),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
ElevatedButton(
|
||||
onPressed: _isListening ? _stopListening : _startListening,
|
||||
child: Text(_isListening ? "🛑 Stop Listening" : "🎤 Start Listening"),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
decoration: const InputDecoration(labelText: "Transcript"),
|
||||
minLines: 2,
|
||||
maxLines: 4,
|
||||
controller: TextEditingController(text: _transcript),
|
||||
onChanged: (val) => _transcript = val,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// 🔴 Live waveform recorder
|
||||
LiveWaveformRecorder(
|
||||
// onStop: (audioPath) {
|
||||
onStop: (path) {
|
||||
// print("🎧 Recorded audio at: $audioPath");
|
||||
// _transcribeAndSend(audioPath);
|
||||
setState(() => _audioPath = path);
|
||||
print("🎧 Recorded audio at: $_audioPath");
|
||||
// don’t need to manually call anymore — the playback screen handles it
|
||||
// _transcribeAndSend(path);
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => VoicePlaybackScreen(audioPath: _audioPath),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
ElevatedButton(
|
||||
onPressed: _sendTranscript,
|
||||
child: const Text("🚀 Send to Agent"),
|
||||
),
|
||||
if (_response != null)
|
||||
Expanded(
|
||||
child: ListView(
|
||||
children: [
|
||||
const SizedBox(height: 12),
|
||||
const Text("🧠 Agent Response", style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
Text(_response.toString()),
|
||||
// // if (_emotion != null)
|
||||
// // Padding(
|
||||
// // padding: const EdgeInsets.only(top: 8),
|
||||
// // child: Text("🌀 Emotion: ${_emotion!['label']} (${_emotion!['score']})"),
|
||||
// // ),
|
||||
// if (_emotion != null)
|
||||
// Padding(
|
||||
// padding: const EdgeInsets.only(top: 8),
|
||||
// child: EmotionBadge(
|
||||
// label: _emotion!['label'],
|
||||
// score: (_emotion!['score'] as num).toDouble(),
|
||||
// ),
|
||||
// ),
|
||||
if (_emotion != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
EmotionBadge(
|
||||
label: _emotion!['label'],
|
||||
score: (_emotion!['score'] as num).toDouble(),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
EmotionIntensityBar(
|
||||
label: _emotion!['label'],
|
||||
score: (_emotion!['score'] as num).toDouble(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_audioPath != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 12),
|
||||
child: WaveformPlayer(audioUrl: _audioPath!), // local path
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
109
mobile-flutter/lib/screens/voice_palyback_screen.dart
Normal file
109
mobile-flutter/lib/screens/voice_palyback_screen.dart
Normal file
@@ -0,0 +1,109 @@
|
||||
// lib/screens/voice_playback_screen.dart
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import '../widgets/waveform_player.dart';
|
||||
import '../widgets/emotion_badge.dart';
|
||||
import '../widgets/emotion_intensity_bar.dart';
|
||||
import '../services/sync_service.dart';
|
||||
import '../services/memory_service.dart';
|
||||
|
||||
class VoicePlaybackScreen extends StatefulWidget {
|
||||
final String audioPath;
|
||||
|
||||
const VoicePlaybackScreen({super.key, required this.audioPath});
|
||||
|
||||
@override
|
||||
State<VoicePlaybackScreen> createState() => _VoicePlaybackScreenState();
|
||||
}
|
||||
|
||||
class _VoicePlaybackScreenState extends State<VoicePlaybackScreen> {
|
||||
String? transcript;
|
||||
Map<String, dynamic>? emotion;
|
||||
Map<String, dynamic>? response;
|
||||
bool loading = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_processAudio();
|
||||
}
|
||||
|
||||
Future<void> _processAudio() async {
|
||||
final result = await SyncService.uploadVoiceFile(widget.audioPath);
|
||||
setState(() {
|
||||
transcript = result["transcript"];
|
||||
emotion = result["emotion"];
|
||||
response = result["response"];
|
||||
loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text("🎧 Voice Playback")),
|
||||
body: loading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: ListView(
|
||||
children: [
|
||||
const Text("▶️ Audio Playback", style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 8),
|
||||
WaveformPlayer(audioUrl: widget.audioPath),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
const Text("📝 Transcript", style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 4),
|
||||
Text(transcript ?? "", style: const TextStyle(fontSize: 14)),
|
||||
|
||||
if (emotion != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
const Text("😊 Emotion", style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 4),
|
||||
EmotionBadge(
|
||||
label: emotion!["label"],
|
||||
score: (emotion!["score"] as num).toDouble(),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
EmotionIntensityBar(
|
||||
label: emotion!["label"],
|
||||
score: (emotion!["score"] as num).toDouble(),
|
||||
),
|
||||
],
|
||||
|
||||
if (response != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
const Text("🧠 Agent Response", style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 4),
|
||||
Text(response.toString(), style: const TextStyle(fontSize: 14)),
|
||||
],
|
||||
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton.icon(
|
||||
icon: const Icon(Icons.save),
|
||||
label: const Text("💾 Save to Memory"),
|
||||
onPressed: () async {
|
||||
try {
|
||||
await MemoryService.saveMemory(
|
||||
transcript: transcript!,
|
||||
response: response!,
|
||||
emotion: emotion!,
|
||||
);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text("✅ Memory saved successfully")),
|
||||
);
|
||||
} catch (e) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text("❌ Failed to save memory")),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
55
mobile-flutter/lib/services/memory_service.dart
Normal file
55
mobile-flutter/lib/services/memory_service.dart
Normal file
@@ -0,0 +1,55 @@
|
||||
// lib/services/memory_service.dart
|
||||
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
class MemoryService {
|
||||
static const String baseUrl = "https://your-api-endpoint.com/api";
|
||||
|
||||
static Future<void> saveMemory({
|
||||
required String transcript,
|
||||
required Map<String, dynamic> response,
|
||||
required Map<String, dynamic> emotion,
|
||||
}) async {
|
||||
// final uri = Uri.parse("https://your-api-endpoint.com/api/memory/save");
|
||||
final uri = Uri.parse("$baseUrl/memory/save");
|
||||
final res = await http.post(
|
||||
uri,
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: json.encode({
|
||||
"transcript": transcript,
|
||||
"response": response,
|
||||
"emotion": emotion,
|
||||
}),
|
||||
);
|
||||
|
||||
if (res.statusCode != 200) {
|
||||
throw Exception("Failed to save memory");
|
||||
}
|
||||
}
|
||||
|
||||
static Future<List<Map<String, dynamic>>> listMemories({int limit = 50}) async {
|
||||
final uri = Uri.parse("$baseUrl/memory/list?limit=$limit");
|
||||
final res = await http.get(uri);
|
||||
|
||||
if (res.statusCode == 200) {
|
||||
final data = json.decode(res.body);
|
||||
return List<Map<String, dynamic>>.from(data["memories"]);
|
||||
} else {
|
||||
throw Exception("Failed to fetch memories");
|
||||
}
|
||||
}
|
||||
|
||||
static Future<List<Map<String, dynamic>>> searchMemories(String query, {int k = 5}) async {
|
||||
final uri = Uri.parse("$baseUrl/memory/search?query=$query&k=$k");
|
||||
final res = await http.get(uri);
|
||||
|
||||
if (res.statusCode == 200) {
|
||||
final data = json.decode(res.body);
|
||||
return List<Map<String, dynamic>>.from(data["matches"]);
|
||||
} else {
|
||||
throw Exception("Failed to search memories");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
25
mobile-flutter/lib/services/model_service.dart
Normal file
25
mobile-flutter/lib/services/model_service.dart
Normal file
@@ -0,0 +1,25 @@
|
||||
// lib/services/model_service.dart
|
||||
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
class ModelService {
|
||||
static const String baseUrl = "https://your-api-endpoint.com/api/admin/model";
|
||||
|
||||
static Future<Map<String, dynamic>> queryModel(String type, String input) async {
|
||||
final uri = Uri.parse("$baseUrl/$type");
|
||||
final res = await http.post(
|
||||
uri,
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: json.encode(type == "embed" || type == "vlm"
|
||||
? {"text": input}
|
||||
: {"prompt": input}),
|
||||
);
|
||||
|
||||
if (res.statusCode == 200) {
|
||||
return json.decode(res.body);
|
||||
} else {
|
||||
throw Exception("Failed to query $type model");
|
||||
}
|
||||
}
|
||||
}
|
||||
113
mobile-flutter/lib/services/sync_service.dart
Normal file
113
mobile-flutter/lib/services/sync_service.dart
Normal file
@@ -0,0 +1,113 @@
|
||||
// lib/services/sync_service.dart
|
||||
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
class SyncService {
|
||||
static const String baseUrl = "https://your-api-endpoint.com/api/sync"; // Replace with actual base URL
|
||||
|
||||
static Future<List<Map<String, dynamic>>> getAgentMemory(String tenantId, String agentRole) async {
|
||||
final uri = Uri.parse("$baseUrl/agent/memory?tenant_id=$tenantId&agent_role=$agentRole");
|
||||
final res = await http.get(uri);
|
||||
if (res.statusCode == 200) {
|
||||
final data = json.decode(res.body);
|
||||
return List<Map<String, dynamic>>.from(data["episodes"] ?? []);
|
||||
} else {
|
||||
throw Exception("Failed to load memory");
|
||||
}
|
||||
}
|
||||
|
||||
static Future<List<Map<String, dynamic>>> searchAgentMemory(String tenantId, String agentRole, String query) async {
|
||||
final uri = Uri.parse("$baseUrl/memory/search?tenantId=$tenantId&agentRole=$agentRole&query=$query");
|
||||
final res = await http.get(uri);
|
||||
|
||||
if (res.statusCode == 200) {
|
||||
final data = json.decode(res.body);
|
||||
return List<Map<String, dynamic>>.from(data["matches"]);
|
||||
} else {
|
||||
throw Exception("Failed to search agent memory");
|
||||
}
|
||||
}
|
||||
|
||||
static Future<List<Map<String, dynamic>>> listAgentMemory(String tenantId, String agentRole, {int limit = 50}) async {
|
||||
final uri = Uri.parse("$baseUrl/memory/list?tenantId=$tenantId&agentRole=$agentRole&limit=$limit");
|
||||
final res = await http.get(uri);
|
||||
|
||||
if (res.statusCode == 200) {
|
||||
final data = json.decode(res.body);
|
||||
return List<Map<String, dynamic>>.from(data["memories"]);
|
||||
} else {
|
||||
throw Exception("Failed to list agent memory");
|
||||
}
|
||||
}
|
||||
|
||||
static Future<Map<String, dynamic>> getTopicGraph(String tenantId, String agentRole) async {
|
||||
final uri = Uri.parse("$baseUrl/memory/topic-graph?tenantId=$tenantId&agentRole=$agentRole");
|
||||
final res = await http.get(uri);
|
||||
|
||||
if (res.statusCode == 200) {
|
||||
return json.decode(res.body);
|
||||
} else {
|
||||
throw Exception("Failed to fetch topic graph");
|
||||
}
|
||||
}
|
||||
|
||||
static Future<List<Map<String, dynamic>>> getHeatmap(String tenantId, String agentRole) async {
|
||||
final uri = Uri.parse("$baseUrl/memory/heatmap?tenantId=$tenantId&agentRole=$agentRole");
|
||||
final res = await http.get(uri);
|
||||
if (res.statusCode == 200) {
|
||||
final data = json.decode(res.body);
|
||||
return List<Map<String, dynamic>>.from(data);
|
||||
} else {
|
||||
throw Exception("Failed to fetch heatmap");
|
||||
}
|
||||
}
|
||||
|
||||
static Future<Map<String, dynamic>> getClusterSummaries(String tenantId, String agentRole) async {
|
||||
final uri = Uri.parse("$baseUrl/memory/cluster-summaries?tenantId=$tenantId&agentRole=$agentRole");
|
||||
final res = await http.get(uri);
|
||||
if (res.statusCode == 200) {
|
||||
return json.decode(res.body) as Map<String, dynamic>;
|
||||
} else {
|
||||
throw Exception("Failed to fetch cluster summaries");
|
||||
}
|
||||
}
|
||||
|
||||
static Future<Map<String, dynamic>> getAgentState(String tenantId, String agentRole) async {
|
||||
final uri = Uri.parse("$baseUrl/agent/state?tenant_id=$tenantId&agent_role=$agentRole");
|
||||
final res = await http.get(uri);
|
||||
if (res.statusCode == 200) {
|
||||
return json.decode(res.body);
|
||||
} else {
|
||||
throw Exception("Failed to load agent state");
|
||||
}
|
||||
}
|
||||
|
||||
static Future<Map<String, dynamic>> getAgentStatus(String tenantId, String agentRole) async {
|
||||
final uri = Uri.parse("$baseUrl/agent/status?tenant_id=$tenantId&agent_role=$agentRole");
|
||||
final res = await http.get(uri);
|
||||
if (res.statusCode == 200) {
|
||||
return json.decode(res.body);
|
||||
} else {
|
||||
throw Exception("Failed to load agent status");
|
||||
}
|
||||
}
|
||||
|
||||
static Future<Map<String, dynamic>> sendVoiceTranscript(String tenantId, String agentRole, String transcript) async {
|
||||
final uri = Uri.parse("https://your-api-endpoint.com/api/agent/voice");
|
||||
final res = await http.post(uri,
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: json.encode({
|
||||
"tenant_id": tenantId,
|
||||
"agent_role": agentRole,
|
||||
"transcript": transcript
|
||||
})
|
||||
);
|
||||
if (res.statusCode == 200) {
|
||||
return json.decode(res.body);
|
||||
} else {
|
||||
throw Exception("Failed to send voice input");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
39
mobile-flutter/lib/widgets/emotion_badge.dart
Normal file
39
mobile-flutter/lib/widgets/emotion_badge.dart
Normal file
@@ -0,0 +1,39 @@
|
||||
// lib/widgets/emotion_badge.dart
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class EmotionBadge extends StatelessWidget {
|
||||
final String label;
|
||||
final double score;
|
||||
|
||||
const EmotionBadge({super.key, required this.label, required this.score});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final mapping = {
|
||||
"joy": ["😊", Color(0xFFFFD700)],
|
||||
"sadness": ["😢", Color(0xFF6495ED)],
|
||||
"anger": ["😠", Color(0xFFDC143C)],
|
||||
"fear": ["😨", Color(0xFF8B008B)],
|
||||
"surprise": ["😲", Color(0xFFFF8C00)],
|
||||
"neutral": ["😐", Color(0xFFA9A9A9)],
|
||||
"love": ["❤️", Color(0xFFFF69B4)],
|
||||
"confusion": ["🤔", Color(0xFF20B2AA)],
|
||||
"disgust": ["🤢", Color(0xFF556B2F)],
|
||||
"pride": ["😌", Color(0xFFDAA520)],
|
||||
};
|
||||
|
||||
final emoji = mapping[label]?[0] ?? "😐";
|
||||
final color = mapping[label]?[1] ?? Colors.grey;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text("$emoji $label (${score.toStringAsFixed(2)})",
|
||||
style: const TextStyle(color: Colors.white)),
|
||||
);
|
||||
}
|
||||
}
|
||||
58
mobile-flutter/lib/widgets/emotion_intensity_bar.dart
Normal file
58
mobile-flutter/lib/widgets/emotion_intensity_bar.dart
Normal file
@@ -0,0 +1,58 @@
|
||||
// lib/widgets/emotion_intensity_bar.dart
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class EmotionIntensityBar extends StatelessWidget {
|
||||
final String label;
|
||||
final double score;
|
||||
|
||||
const EmotionIntensityBar({super.key, required this.label, required this.score});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final mapping = {
|
||||
"joy": ["😊", Color(0xFFFFD700)],
|
||||
"sadness": ["😢", Color(0xFF6495ED)],
|
||||
"anger": ["😠", Color(0xFFDC143C)],
|
||||
"fear": ["😨", Color(0xFF8B008B)],
|
||||
"surprise": ["😲", Color(0xFFFF8C00)],
|
||||
"neutral": ["😐", Color(0xFFA9A9A9)],
|
||||
"love": ["❤️", Color(0xFFFF69B4)],
|
||||
"confusion": ["🤔", Color(0xFF20B2AA)],
|
||||
"disgust": ["🤢", Color(0xFF556B2F)],
|
||||
"pride": ["😌", Color(0xFFDAA520)],
|
||||
};
|
||||
|
||||
final emoji = mapping[label]?[0] ?? "😐";
|
||||
final color = mapping[label]?[1] ?? Colors.grey;
|
||||
final widthPercent = (score.clamp(0, 1) * 100).toDouble();
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text("$emoji $label (${score.toStringAsFixed(2)})", style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 4),
|
||||
Stack(
|
||||
children: [
|
||||
Container(
|
||||
height: 10,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[300],
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
height: 10,
|
||||
width: MediaQuery.of(context).size.width * (widthPercent / 100),
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
73
mobile-flutter/lib/widgets/live_waveform_recorder.dart
Normal file
73
mobile-flutter/lib/widgets/live_waveform_recorder.dart
Normal file
@@ -0,0 +1,73 @@
|
||||
// lib/widgets/live_waveform_recorder.dart
|
||||
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:audio_waveforms/audio_waveforms.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
class LiveWaveformRecorder extends StatefulWidget {
|
||||
final void Function(String audioPath)? onStop;
|
||||
|
||||
const LiveWaveformRecorder({super.key, this.onStop});
|
||||
|
||||
@override
|
||||
State<LiveWaveformRecorder> createState() => _LiveWaveformRecorderState();
|
||||
}
|
||||
|
||||
class _LiveWaveformRecorderState extends State<LiveWaveformRecorder> {
|
||||
late final RecorderController _controller;
|
||||
bool _isRecording = false;
|
||||
String? _audioPath;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = RecorderController()
|
||||
..updateFrequency = const Duration(milliseconds: 100)
|
||||
..waveStyle = const WaveStyle(
|
||||
waveColor: Colors.indigo,
|
||||
showMiddleLine: false,
|
||||
extendWaveform: true,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _startRecording() async {
|
||||
final micStatus = await Permission.microphone.request();
|
||||
if (!micStatus.isGranted) return;
|
||||
|
||||
final dir = await getApplicationDocumentsDirectory();
|
||||
final path = "${dir.path}/voice_input.wav";
|
||||
_audioPath = path;
|
||||
|
||||
await _controller.record(path: path);
|
||||
setState(() => _isRecording = true);
|
||||
}
|
||||
|
||||
Future<void> _stopRecording() async {
|
||||
await _controller.stop();
|
||||
setState(() => _isRecording = false);
|
||||
if (_audioPath != null && widget.onStop != null) {
|
||||
widget.onStop!(_audioPath!);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
AudioWaveforms(
|
||||
enableGesture: false,
|
||||
size: Size(MediaQuery.of(context).size.width, 100),
|
||||
recorderController: _controller,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
ElevatedButton.icon(
|
||||
icon: Icon(_isRecording ? Icons.stop : Icons.mic),
|
||||
label: Text(_isRecording ? "🛑 Stop Recording" : "🎙️ Start Recording"),
|
||||
onPressed: _isRecording ? _stopRecording : _startRecording,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
50
mobile-flutter/lib/widgets/waveform_player.dart
Normal file
50
mobile-flutter/lib/widgets/waveform_player.dart
Normal file
@@ -0,0 +1,50 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:waveform_flutter/waveform_flutter.dart';
|
||||
import 'package:audioplayers/audioplayers.dart';
|
||||
|
||||
class WaveformPlayer extends StatefulWidget {
|
||||
final String audioUrl;
|
||||
|
||||
const WaveformPlayer({super.key, required this.audioUrl});
|
||||
|
||||
@override
|
||||
State<WaveformPlayer> createState() => _WaveformPlayerState();
|
||||
}
|
||||
|
||||
class _WaveformPlayerState extends State<WaveformPlayer> {
|
||||
final player = AudioPlayer();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
player.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _togglePlay() async {
|
||||
final state = player.state;
|
||||
if (state == PlayerState.playing) {
|
||||
await player.pause();
|
||||
} else {
|
||||
await player.play(UrlSource(widget.audioUrl));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Waveform(
|
||||
samples: List.generate(100, (i) => (i % 10) * 0.1), // placeholder
|
||||
height: 60,
|
||||
width: MediaQuery.of(context).size.width,
|
||||
color: Colors.indigo,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
ElevatedButton(
|
||||
onPressed: _togglePlay,
|
||||
child: const Text("▶️ Play / Pause"),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -14,12 +14,22 @@ class EngineUpdate(BaseModel):
|
||||
llm: str | None = None
|
||||
slm: str | None = None
|
||||
embedding: str | None = None
|
||||
vlm: str | None = None
|
||||
moe: str | None = None
|
||||
lcm: str | None = None
|
||||
lam: str | None = None
|
||||
mlm: str | None = None
|
||||
|
||||
@router.get("/engines")
|
||||
def get_engines():
|
||||
return {
|
||||
"llm_engine": config.LLM_ENGINE,
|
||||
"slm_engine": config.SLM_ENGINE
|
||||
"slm_engine": config.SLM_ENGINE,
|
||||
"vlm_engine": getattr(config, "VLM_ENGINE", "clip"),
|
||||
"moe_engine": getattr(config, "MOE_ENGINE", "default"),
|
||||
"lcm_engine": getattr(config, "LCM_ENGINE", "default"),
|
||||
"lam_engine": getattr(config, "LAM_ENGINE", "default"),
|
||||
"mlm_engine": getattr(config, "MLM_ENGINE", "bert-base-uncased"),
|
||||
}
|
||||
|
||||
@router.post("/engines")
|
||||
@@ -30,12 +40,27 @@ def update_engines(update: EngineUpdate):
|
||||
config.SLM_ENGINE = update.slm
|
||||
if update.embedding:
|
||||
config.EMBEDDING_ENGINE = update.embedding
|
||||
if update.vlm:
|
||||
config.VLM_ENGINE = update.vlm
|
||||
if update.moe:
|
||||
config.MOE_ENGINE = update.moe
|
||||
if update.lcm:
|
||||
config.LCM_ENGINE = update.lcm
|
||||
if update.lam:
|
||||
config.LAM_ENGINE = update.lam
|
||||
if update.mlm:
|
||||
config.MLM_ENGINE = update.mlm
|
||||
|
||||
return {
|
||||
"status": "updated",
|
||||
"llm_engine": config.LLM_ENGINE,
|
||||
"slm_engine": config.SLM_ENGINE,
|
||||
"embedding_engine": config.EMBEDDING_ENGINE
|
||||
"embedding_engine": config.EMBEDDING_ENGINE,
|
||||
"vlm_engine": getattr(config, "VLM_ENGINE", "clip"),
|
||||
"moe_engine": getattr(config, "MOE_ENGINE", "default"),
|
||||
"lcm_engine": getattr(config, "LCM_ENGINE", "default"),
|
||||
"lam_engine": getattr(config, "LAM_ENGINE", "default"),
|
||||
"mlm_engine": getattr(config, "MLM_ENGINE", "bert-base-uncased"),
|
||||
}
|
||||
|
||||
##INFO: to support 'goal_heatmap' configuration
|
||||
|
||||
@@ -1,21 +1,44 @@
|
||||
# routes/inference_routes.py
|
||||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel
|
||||
from models.llm_loader import get_llm
|
||||
from agents.orchestrator import route_request
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
llm = get_llm()
|
||||
# llm = get_llm()
|
||||
|
||||
class EdgePrompt(BaseModel):
|
||||
prompt: str
|
||||
|
||||
# @router.post("/llm/infer-edge")
|
||||
# def infer_edge_label(data: EdgePrompt):
|
||||
# label = llm(data.prompt).strip()
|
||||
# return {"label": label}
|
||||
@router.post("/llm/infer-edge")
|
||||
def infer_edge_label(data: EdgePrompt):
|
||||
label = llm(data.prompt).strip()
|
||||
llm = get_model("llm")
|
||||
label = llm.generate(data.prompt).strip()
|
||||
return {"label": label}
|
||||
|
||||
# @router.post("/admin/infer")
|
||||
# def infer(prompt: str, context: dict = {}):
|
||||
# return route_request(prompt, context)
|
||||
@router.post("/admin/infer")
|
||||
def infer(prompt: str, context: dict = {}):
|
||||
def infer(prompt: str = Body(...), context: dict = Body(default={})):
|
||||
return route_request(prompt, context)
|
||||
|
||||
# -----------------------------
|
||||
# New unified inference route
|
||||
# -----------------------------
|
||||
@router.post("/admin/infer/{model_type}")
|
||||
def infer_with_model(model_type: str, prompt: str = Body(...)):
|
||||
"""
|
||||
Run inference with a specific model type (llm, slm, vlm, moe, lcm, lam, mlm, embed).
|
||||
"""
|
||||
model = get_model(model_type)
|
||||
if model_type in ["embed", "vlm"]:
|
||||
vector = model.embed(prompt)
|
||||
return {"model_type": model_type, "vector": vector}
|
||||
else:
|
||||
output = model.generate(prompt)
|
||||
return {"model_type": model_type, "output": output}
|
||||
|
||||
@@ -1,21 +1,72 @@
|
||||
# routes/model_router_routes.py
|
||||
|
||||
from fastapi import APIRouter
|
||||
from models.model_router import get_routed_llm, get_routed_slm, get_routed_embedding
|
||||
# from models.model_router import get_routed_llm, get_routed_slm, get_routed_embedding
|
||||
from models.registry import get_model
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# @router.post("/admin/model/llm")
|
||||
# def route_llm(prompt: str):
|
||||
# model = get_routed_llm(prompt)
|
||||
# return {"engine": str(model)}
|
||||
@router.post("/admin/model/llm")
|
||||
def route_llm(prompt: str):
|
||||
model = get_routed_llm(prompt)
|
||||
return {"engine": str(model)}
|
||||
def route_llm(prompt: str = Body(...)):
|
||||
model = get_model("llm")
|
||||
output = model.generate(prompt)
|
||||
return {"engine": "llm", "output": output}
|
||||
|
||||
# @router.post("/admin/model/slm")
|
||||
# def route_slm(prompt: str):
|
||||
# model = get_routed_slm(prompt)
|
||||
# return {"engine": str(model)}
|
||||
@router.post("/admin/model/slm")
|
||||
def route_slm(prompt: str):
|
||||
model = get_routed_slm(prompt)
|
||||
return {"engine": str(model)}
|
||||
def route_slm(prompt: str = Body(...)):
|
||||
model = get_model("slm")
|
||||
output = model.generate(prompt)
|
||||
return {"engine": "slm", "output": output}
|
||||
|
||||
# @router.post("/admin/model/embed")
|
||||
# def route_embedding(text: str):
|
||||
# model = get_routed_embedding(text)
|
||||
# return {"engine": str(model)}
|
||||
@router.post("/admin/model/embed")
|
||||
def route_embedding(text: str):
|
||||
model = get_routed_embedding(text)
|
||||
return {"engine": str(model)}
|
||||
def route_embedding(text: str = Body(...)):
|
||||
model = get_model("embedding")
|
||||
vector = model.embed(text)
|
||||
return {"engine": "embedding", "vector": vector}
|
||||
|
||||
@router.post("/admin/model/vlm")
|
||||
def route_vlm(text: str = Body(...)):
|
||||
"""Vision-Language embedding (text only for now)."""
|
||||
model = get_model("vlm")
|
||||
vector = model.embed(text)
|
||||
return {"engine": "vlm", "vector": vector}
|
||||
|
||||
@router.post("/admin/model/moe")
|
||||
def route_moe(prompt: str = Body(...)):
|
||||
"""Mixture-of-Experts stub."""
|
||||
model = get_model("moe")
|
||||
output = model.generate(prompt)
|
||||
return {"engine": "moe", "output": output}
|
||||
|
||||
@router.post("/admin/model/lcm")
|
||||
def route_lcm(prompt: str = Body(...)):
|
||||
"""Latent Consistency Model stub."""
|
||||
model = get_model("lcm")
|
||||
output = model.generate(prompt)
|
||||
return {"engine": "lcm", "output": output}
|
||||
|
||||
@router.post("/admin/model/lam")
|
||||
def route_lam(prompt: str = Body(...)):
|
||||
"""Language Alignment Model stub."""
|
||||
model = get_model("lam")
|
||||
output = model.generate(prompt)
|
||||
return {"engine": "lam", "output": output}
|
||||
|
||||
@router.post("/admin/model/mlm")
|
||||
def route_mlm(prompt: str = Body(...)):
|
||||
"""Masked Language Model (fill-mask)."""
|
||||
model = get_model("mlm")
|
||||
output = model.generate(prompt)
|
||||
return {"engine": "mlm", "output": output}
|
||||
|
||||
@@ -6,15 +6,30 @@ import axios from "axios";
|
||||
export default function ModelRouterPanel() {
|
||||
const [prompt, setPrompt] = useState("");
|
||||
const [result, setResult] = useState(null);
|
||||
const [modelType, setModelType] = useState("llm");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const modelTypes = ["llm", "slm", "embed", "vlm", "moe", "lcm", "lam", "mlm"];
|
||||
|
||||
// const runInference = async () => {
|
||||
// const res = await axios.post("/api/admin/infer", { prompt });
|
||||
// setResult(res.data);
|
||||
// };
|
||||
const runInference = async () => {
|
||||
const res = await axios.post("/api/admin/infer", { prompt });
|
||||
setResult(res.data);
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await axios.post(`/api/admin/infer/${modelType}`, { prompt });
|
||||
setResult(res.data);
|
||||
} catch (err) {
|
||||
setResult({ error: err.message });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold">🧠 Auto Model Router</h3>
|
||||
{/* <h3 className="text-lg font-semibold">🧠 Auto Model Router</h3>
|
||||
<textarea value={prompt} onChange={(e) => setPrompt(e.target.value)} className="px-2 py-1 border rounded text-sm h-24 w-full" />
|
||||
<button onClick={runInference} className="px-3 py-1 bg-blue-600 text-white rounded text-sm">Run</button>
|
||||
|
||||
@@ -24,7 +39,62 @@ export default function ModelRouterPanel() {
|
||||
<p><strong>Profile:</strong> {result.profile}</p>
|
||||
<pre className="text-xs bg-white p-2 rounded whitespace-pre-wrap">{result.response}</pre>
|
||||
</div>
|
||||
)} */}
|
||||
|
||||
<h3 className="text-lg font-semibold">🤖 Model Selector</h3>
|
||||
|
||||
<div className="flex space-x-4">
|
||||
<select
|
||||
value={modelType}
|
||||
onChange={(e) => setModelType(e.target.value)}
|
||||
className="border p-2 rounded"
|
||||
>
|
||||
{modelTypes.map((m) => (
|
||||
<option key={m} value={m}>
|
||||
{m.toUpperCase()}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
onClick={runInference}
|
||||
className="px-3 py-1 bg-blue-600 text-white rounded text-sm"
|
||||
>
|
||||
Run
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<textarea
|
||||
value={prompt}
|
||||
onChange={(e) => setPrompt(e.target.value)}
|
||||
className="px-2 py-1 border rounded text-sm h-24 w-full"
|
||||
placeholder="Enter prompt or text..."
|
||||
/>
|
||||
|
||||
{loading && <p className="text-gray-500">Running {modelType}...</p>}
|
||||
|
||||
{result && (
|
||||
<div className="bg-gray-100 p-4 rounded shadow mt-2">
|
||||
{result.error ? (
|
||||
<p className="text-red-600">Error: {result.error}</p>
|
||||
) : (
|
||||
<>
|
||||
<p><strong>Model Type:</strong> {result.model_type || modelType}</p>
|
||||
{result.engine && <p><strong>Engine:</strong> {result.engine}</p>}
|
||||
{result.output && (
|
||||
<pre className="text-xs bg-white p-2 rounded whitespace-pre-wrap">
|
||||
{result.output}
|
||||
</pre>
|
||||
)}
|
||||
{result.vector && (
|
||||
<pre className="text-xs bg-white p-2 rounded whitespace-pre-wrap">
|
||||
{JSON.stringify(result.vector, null, 2)}
|
||||
</pre>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -5,57 +5,99 @@ import axios from "axios";
|
||||
|
||||
const API = "http://localhost:8000/config/engines";
|
||||
|
||||
// export default function EngineSwitcher() {
|
||||
// const [llm, setLlm] = useState("");
|
||||
// const [slm, setSlm] = useState("");
|
||||
// const [embedding, setEmbedding] = useState("");
|
||||
// const [status, setStatus] = useState("");
|
||||
|
||||
// useEffect(() => {
|
||||
// axios.get(API).then((res) => {
|
||||
// setLlm(res.data.llm_engine);
|
||||
// setSlm(res.data.slm_engine);
|
||||
// setEmbedding(res.data.embedding_engine);
|
||||
// });
|
||||
// }, []);
|
||||
|
||||
// const handleUpdate = async () => {
|
||||
// const res = await axios.post(API, { llm, slm, embedding });
|
||||
// setStatus(res.data.status);
|
||||
// };
|
||||
|
||||
// return (
|
||||
// <div className="bg-white p-6 rounded shadow">
|
||||
// <h2 className="text-xl font-semibold mb-4">🧠 Engine Switcher</h2>
|
||||
// <div className="space-y-4">
|
||||
// <div>
|
||||
// <label className="block font-medium">LLM Engine</label>
|
||||
// <select value={llm} onChange={(e) => setLlm(e.target.value)} className="border p-2 rounded w-full">
|
||||
// <option value="ollama">Ollama</option>
|
||||
// <option value="llama.cpp">llama.cpp</option>
|
||||
// <option value="vllm">vLLM</option>
|
||||
// </select>
|
||||
// </div>
|
||||
// <div>
|
||||
// <label className="block font-medium">SLM Engine</label>
|
||||
// <select value={slm} onChange={(e) => setSlm(e.target.value)} className="border p-2 rounded w-full">
|
||||
// <option value="phi-3">Phi-3</option>
|
||||
// <option value="gemma">Gemma</option>
|
||||
// </select>
|
||||
// </div>
|
||||
// <div>
|
||||
// <label className="block font-medium">Embedding Engine</label>
|
||||
// <select value={embedding} onChange={(e) => setEmbedding(e.target.value)} className="border p-2 rounded w-full">
|
||||
// <option value="huggingface">HuggingFace</option>
|
||||
// <option value="gpt4all">GPT4All</option>
|
||||
// </select>
|
||||
// </div>
|
||||
// <button onClick={handleUpdate} className="bg-blue-500 text-white px-4 py-2 rounded">
|
||||
// Update Engines
|
||||
// </button>
|
||||
// {status && <p className="mt-4 text-green-600 font-medium">{status}</p>}
|
||||
// </div>
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
|
||||
export default function EngineSwitcher() {
|
||||
const [llm, setLlm] = useState("");
|
||||
const [slm, setSlm] = useState("");
|
||||
const [embedding, setEmbedding] = useState("");
|
||||
const [engines, setEngines] = useState({});
|
||||
const [status, setStatus] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
axios.get(API).then((res) => {
|
||||
setLlm(res.data.llm_engine);
|
||||
setSlm(res.data.slm_engine);
|
||||
setEmbedding(res.data.embedding_engine);
|
||||
});
|
||||
axios.get(API).then((res) => setEngines(res.data));
|
||||
}, []);
|
||||
|
||||
const handleUpdate = async () => {
|
||||
const res = await axios.post(API, { llm, slm, embedding });
|
||||
const res = await axios.post(API, engines);
|
||||
setStatus(res.data.status);
|
||||
};
|
||||
|
||||
const handleChange = (key, value) => {
|
||||
setEngines((prev) => ({ ...prev, [key]: value }));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white p-6 rounded shadow">
|
||||
<h2 className="text-xl font-semibold mb-4">🧠 Engine Switcher</h2>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block font-medium">LLM Engine</label>
|
||||
<select value={llm} onChange={(e) => setLlm(e.target.value)} className="border p-2 rounded w-full">
|
||||
<option value="ollama">Ollama</option>
|
||||
<option value="llama.cpp">llama.cpp</option>
|
||||
<option value="vllm">vLLM</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block font-medium">SLM Engine</label>
|
||||
<select value={slm} onChange={(e) => setSlm(e.target.value)} className="border p-2 rounded w-full">
|
||||
<option value="phi-3">Phi-3</option>
|
||||
<option value="gemma">Gemma</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block font-medium">Embedding Engine</label>
|
||||
<select value={embedding} onChange={(e) => setEmbedding(e.target.value)} className="border p-2 rounded w-full">
|
||||
<option value="huggingface">HuggingFace</option>
|
||||
<option value="gpt4all">GPT4All</option>
|
||||
</select>
|
||||
</div>
|
||||
<button onClick={handleUpdate} className="bg-blue-500 text-white px-4 py-2 rounded">
|
||||
{Object.keys(engines).map((key) => (
|
||||
<div key={key}>
|
||||
<label className="block font-medium">{key.replace("_engine", "").toUpperCase()} Engine</label>
|
||||
<input
|
||||
value={engines[key]}
|
||||
onChange={(e) => handleChange(key, e.target.value)}
|
||||
className="border p-2 rounded w-full"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<button
|
||||
onClick={handleUpdate}
|
||||
className="bg-blue-500 text-white px-4 py-2 rounded"
|
||||
>
|
||||
Update Engines
|
||||
</button>
|
||||
{status && <p className="mt-4 text-green-600 font-medium">{status}</p>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user