Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions flutter_app/lib/src/agent_home_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,14 @@ class _AgentHomePageState extends State<AgentHomePage>
_inputController.selection = TextSelection.collapsed(offset: text.length);
}

void _prefillChatPrompt(String text) {
setState(() {
_inputController.text = text;
_inputController.selection = TextSelection.collapsed(offset: text.length);
_selectedView = AppView.chat;
});
}

Future<void> _sendMessage() async {
final text = _inputController.text.trim();
if (text.isEmpty || _isSending) return;
Expand Down Expand Up @@ -298,6 +306,7 @@ class _AgentHomePageState extends State<AgentHomePage>
onDeleteSession: _deleteSession,
onSuggestionSelected: _useSuggestion,
onSend: _sendMessage,
onAskAssistant: _prefillChatPrompt,
onLogout: widget.onLogout,
onSaveProfile: widget.onSaveProfile,
onSaveAgentConfig: _saveAgentConfig,
Expand Down
78 changes: 78 additions & 0 deletions flutter_app/lib/src/map_location_models.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import 'dart:convert';

class MapLocation {
const MapLocation({
required this.name,
required this.latitude,
required this.longitude,
this.address,
this.category,
this.source = 'nominatim',
});

final String name;
final double latitude;
final double longitude;
final String? address;
final String? category;
final String source;

String get coordinateText =>
'${latitude.toStringAsFixed(5)}, ${longitude.toStringAsFixed(5)}';

String get mapsQuery => Uri.encodeComponent('$latitude,$longitude ($name)');

String assistantPrompt() {
final buffer = StringBuffer()
..writeln('I selected this destination in StudyOS Maps:')
..writeln('- Name: $name')
..writeln('- Coordinates: $coordinateText')
..writeln('- Source: $source');
if (address != null && address!.isNotEmpty) {
buffer.writeln('- Address: $address');
}
if (category != null && category!.isNotEmpty) {
buffer.writeln('- Category: $category');
}
buffer
..writeln()
..writeln(
'Please explain what is useful to know about this place, how I could get there from my current study context, and any navigation options or caveats.',
);
return buffer.toString().trim();
}

static MapLocation? fromNominatimJson(Map<String, Object?> json) {
final lat = double.tryParse(json['lat']?.toString() ?? '');
final lon = double.tryParse(json['lon']?.toString() ?? '');
final displayName = json['display_name']?.toString().trim();
if (lat == null ||
lon == null ||
displayName == null ||
displayName.isEmpty) {
return null;
}
final namedParts = displayName.split(',');
final name = namedParts.first.trim();
return MapLocation(
name: name.isEmpty ? displayName : name,
latitude: lat,
longitude: lon,
address: displayName,
category: json['type']?.toString(),
);
}
}

List<MapLocation> mapLocationsFromNominatim(String body) {
final decoded = jsonDecode(body);
if (decoded is! List) return const <MapLocation>[];
return decoded
.whereType<Map>()
.map(
(item) =>
MapLocation.fromNominatimJson(Map<String, Object?>.from(item)),
)
.whereType<MapLocation>()
.toList(growable: false);
}
50 changes: 50 additions & 0 deletions flutter_app/lib/src/map_search_client.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import 'package:http/http.dart' as http;

import 'map_location_models.dart';

class MapSearchClient {
MapSearchClient({http.Client? client})
: _client = client ?? http.Client(),
_ownsClient = client == null;

final http.Client _client;
final bool _ownsClient;

Future<List<MapLocation>> search(String query) async {
final trimmed = query.trim();
if (trimmed.isEmpty) return const <MapLocation>[];
final uri = Uri.https('nominatim.openstreetmap.org', '/search', {
'format': 'jsonv2',
'limit': '8',
'addressdetails': '1',
'bounded': '1',
'viewbox': '8.93,48.57,9.16,48.47',
'q': '$trimmed Tuebingen',
});
final response = await _client.get(
uri,
headers: const <String, String>{
'User-Agent': 'StudyOS Agent course prototype',
},
);
if (response.statusCode < 200 || response.statusCode >= 300) {
throw MapSearchException(
'Search failed with HTTP ${response.statusCode}.',
);
}
return mapLocationsFromNominatim(response.body);
}

void close() {
if (_ownsClient) _client.close();
}
}

class MapSearchException implements Exception {
const MapSearchException(this.message);

final String message;

@override
String toString() => message;
}
2 changes: 1 addition & 1 deletion flutter_app/lib/src/models.dart
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ class ChatMessage {
}
}

enum AppView { home, chat, schedule, mail, campus, memories, settings }
enum AppView { home, chat, schedule, mail, maps, campus, memories, settings }

enum AgentProvider { local, cloud }

Expand Down
5 changes: 5 additions & 0 deletions flutter_app/lib/src/views/agent_selected_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'campus_view.dart';
import 'chat_view.dart';
import 'home_view.dart';
import 'mail_view.dart';
import 'maps_view.dart';
import 'memories_view.dart';
import 'schedule_view.dart';
import 'settings_view.dart';
Expand All @@ -22,6 +23,7 @@ class AgentSelectedView extends StatelessWidget {
required this.compactMessages,
required this.onSuggestionSelected,
required this.onSend,
required this.onAskAssistant,
required this.onSelectView,
required this.worldState,
required this.memoryText,
Expand Down Expand Up @@ -50,6 +52,7 @@ class AgentSelectedView extends StatelessWidget {
final bool compactMessages;
final ValueChanged<String> onSuggestionSelected;
final VoidCallback onSend;
final ValueChanged<String> onAskAssistant;
final ValueChanged<AppView> onSelectView;
final Map<String, Object?> worldState;
final String memoryText;
Expand Down Expand Up @@ -77,6 +80,7 @@ class AgentSelectedView extends StatelessWidget {
memoryText: memoryText,
timetable: timetable,
onOpenMail: () => onSelectView(AppView.mail),
onOpenMaps: () => onSelectView(AppView.maps),
onOpenCampus: () => onSelectView(AppView.campus),
onOpenSchedule: () => onSelectView(AppView.schedule),
),
Expand All @@ -97,6 +101,7 @@ class AgentSelectedView extends StatelessWidget {
onRefresh: onRefreshTimetable,
),
AppView.mail => MailView(profile: profile),
AppView.maps => MapsView(onAskAssistant: onAskAssistant),
AppView.campus => CampusView(profile: profile),
AppView.memories => MemoriesView(
worldState: worldState,
Expand Down
15 changes: 15 additions & 0 deletions flutter_app/lib/src/views/home_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class HomeView extends StatelessWidget {
required this.memoryText,
required this.timetable,
required this.onOpenMail,
required this.onOpenMaps,
required this.onOpenCampus,
required this.onOpenSchedule,
super.key,
Expand All @@ -21,6 +22,7 @@ class HomeView extends StatelessWidget {
final String memoryText;
final TimetableSnapshot? timetable;
final VoidCallback onOpenMail;
final VoidCallback onOpenMaps;
final VoidCallback onOpenCampus;
final VoidCallback onOpenSchedule;

Expand Down Expand Up @@ -69,6 +71,11 @@ class HomeView extends StatelessWidget {
label: 'Mail',
value: profile == null ? 'Sign in needed' : 'Local tools ready',
),
const _HomeStatusItem(
icon: Icons.map_outlined,
label: 'Map',
value: 'Tübingen',
),
],
),
const SizedBox(height: StudyOsSpacing.lg),
Expand All @@ -86,6 +93,14 @@ class HomeView extends StatelessWidget {
onTap: onOpenMail,
),
const SizedBox(height: StudyOsSpacing.md),
_HomeCard(
key: const ValueKey<String>('home-maps-card'),
icon: Icons.map_outlined,
title: 'Navigate',
body: 'Search destinations and ask StudyOS about routes.',
onTap: onOpenMaps,
),
const SizedBox(height: StudyOsSpacing.md),
_HomeCard(
key: const ValueKey<String>('home-campus-card'),
icon: Icons.restaurant_outlined,
Expand Down
Loading