Updates for 'mobile-flutter' libraries with 'Model Selector UI'

This commit is contained in:
2025-11-26 10:41:07 +09:00
parent 0afe3f49ff
commit 25011235ee
23 changed files with 1831 additions and 53 deletions

3
.gitignore vendored
View File

@@ -321,3 +321,6 @@ vite.config.ts.timestamp-*
.cursor/rules/todo2.mdc
.cursor/mcp.json
.todo2/
# Exclude 'mobile-flutter/lib'
!mobile-flutter/lib

View 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"),
),
],
),
),
);
}
}

View 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"])),
],
),
);
}
}

View 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)),
],
),
),
],
),
),
);
},
),
);
}
}

View 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()),
),
),
],
),
),
);
}
}

View 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"),
],
),
],
),
),
);
}
}

View 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)),
],
),
),
);
},
),
),
],
),
);
}
}

View 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),
),
),
),
],
),
),
);
}
}

View 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()),
);
},
),
),
);
}
}

View 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");
// dont 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
),
],
),
),
);
}
}

View 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")),
);
}
},
),
],
),
),
);
}
}

View 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");
}
}
}

View 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");
}
}
}

View 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");
}
}
}

View 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)),
);
}
}

View 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),
),
),
],
),
],
);
}
}

View 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,
),
],
);
}
}

View 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"),
),
],
);
}
}

View File

@@ -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

View File

@@ -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}

View File

@@ -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}

View File

@@ -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>
);
}

View File

@@ -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>
);
}
}