Skip to content
Open
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
5 changes: 5 additions & 0 deletions flutter_app/lib/src/app_router.dart
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,11 @@ class _HomeRoute extends StatelessWidget {
'/chat?prompt=What%20is%20good%20at%20the%20Mensa%20today%3F',
),
onOpenSchedule: () => context.go('/plan'),
onAskAssistant: (prompt) => context.push(
Uri(path: '/chat', queryParameters: <String, String>{
'prompt': prompt,
}).toString(),
),
onRefresh: controller.refreshHomeFeed,
),
);
Expand Down
122 changes: 122 additions & 0 deletions flutter_app/lib/src/feed_summary.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ enum HomeFeedSourceStatus { fresh, stale, unavailable }

enum UrgentItemSeverity { info, warning }

enum FeedTemplateCardKind { schedule, highlight, email }

class DailySummary {
const DailySummary({required this.title, required this.body});

Expand Down Expand Up @@ -56,12 +58,87 @@ class HomeFeedSourceFreshness {
}
}

class FeedScheduleCard {
const FeedScheduleCard({
required this.id,
required this.courseName,
required this.timeRange,
required this.hoursUntil,
this.type,
this.address,
this.llmSummary,
});

final String id;
final String courseName;
final String? type;
final String timeRange;
final int hoursUntil;
final String? address;

/// Template slot intentionally left for local LLM generated prep text.
final String? llmSummary;

String get timeToNextLabel {
if (hoursUntil <= 0) return 'now';
if (hoursUntil == 1) return '1 h';
return '$hoursUntil h';
}
}

class FeedArticleCard {
const FeedArticleCard({
required this.id,
required this.title,
required this.sourceLabel,
required this.body,
this.imageUrl,
this.publishedAt,
this.llmSummary,
});

final String id;
final String title;
final String sourceLabel;
final String body;
final String? imageUrl;
final DateTime? publishedAt;

/// Template slot intentionally left for local LLM generated summary text.
final String? llmSummary;
}

class FeedEmailCard {
const FeedEmailCard({
required this.id,
required this.subject,
required this.sender,
required this.preview,
this.receivedAt,
this.isUnread = false,
this.llmSummary,
});

final String id;
final String subject;
final String sender;
final String preview;
final DateTime? receivedAt;
final bool isUnread;

/// Template slot intentionally left for local LLM extracted action items.
final String? llmSummary;
}

class HomeFeedSnapshot {
const HomeFeedSnapshot({
required this.summary,
required this.nextAction,
required this.urgentItems,
required this.sources,
required this.todaySchedule,
required this.highlights,
required this.emails,
required this.generatedAt,
});

Expand Down Expand Up @@ -112,6 +189,7 @@ class HomeFeedSnapshot {
now: now,
),
);
final todaySchedule = _scheduleCardsFor(timetable: timetable, now: now);

return HomeFeedSnapshot(
summary: summary,
Expand All @@ -120,6 +198,9 @@ class HomeFeedSnapshot {
sources: List<HomeFeedSourceFreshness>.unmodifiable(
_sourcesFor(timetable: timetable, memoryText: memoryText, now: now),
),
todaySchedule: List<FeedScheduleCard>.unmodifiable(todaySchedule),
highlights: const <FeedArticleCard>[],
emails: const <FeedEmailCard>[],
generatedAt: now,
);
}
Expand All @@ -128,6 +209,9 @@ class HomeFeedSnapshot {
final NextAction nextAction;
final List<UrgentItem> urgentItems;
final List<HomeFeedSourceFreshness> sources;
final List<FeedScheduleCard> todaySchedule;
final List<FeedArticleCard> highlights;
final List<FeedEmailCard> emails;
final DateTime generatedAt;

bool get hasUrgentItems => urgentItems.isNotEmpty;
Expand All @@ -136,6 +220,44 @@ class HomeFeedSnapshot {
String get generatedAtLabel => _time(generatedAt);
}

List<FeedScheduleCard> _scheduleCardsFor({
required TimetableSnapshot? timetable,
required DateTime now,
}) {
final events = timetable?.eventsOn(now) ?? const <LectureEvent>[];
return events.map((event) {
final hoursUntil = event.start.isAfter(now)
? (event.start.difference(now).inMinutes / 60).ceil()
: 0;
return FeedScheduleCard(
id: event.id,
courseName: event.title,
type: _lectureType(event.detail),
timeRange: event.timeRangeText,
hoursUntil: hoursUntil,
address: event.location,
llmSummary: null,
);
}).toList()..sort((a, b) {
final hours = a.hoursUntil.compareTo(b.hoursUntil);
if (hours != 0) return hours;
return a.courseName.compareTo(b.courseName);
});
}

String? _lectureType(String? detail) {
final normalized = detail?.toLowerCase();
if (normalized == null || normalized.isEmpty) return null;
if (normalized.contains('tutorial') || normalized.contains('übung')) {
return 'Tutorial';
}
if (normalized.contains('lecture') || normalized.contains('vorlesung')) {
return 'Lecture';
}
if (normalized.contains('seminar')) return 'Seminar';
return null;
}

class FeedMessage {
const FeedMessage({required this.title, required this.body});

Expand Down
8 changes: 7 additions & 1 deletion flutter_app/lib/src/views/home_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class HomeView extends StatelessWidget {
required this.onOpenMaps,
required this.onOpenCampus,
required this.onOpenSchedule,
required this.onAskAssistant,
required this.onRefresh,
super.key,
});
Expand All @@ -37,6 +38,7 @@ class HomeView extends StatelessWidget {
final VoidCallback onOpenMaps;
final VoidCallback onOpenCampus;
final VoidCallback onOpenSchedule;
final ValueChanged<String> onAskAssistant;
final Future<void> Function() onRefresh;

@override
Expand All @@ -54,7 +56,11 @@ class HomeView extends StatelessWidget {
const SizedBox(height: StudyOsSpacing.xxl),
_SectionLabel(label: 'For you'),
const SizedBox(height: StudyOsSpacing.sm),
ProactiveFeedSection(snapshot: snapshot, onRefresh: onRefresh),
ProactiveFeedSection(
snapshot: snapshot,
onRefresh: onRefresh,
onAskAssistant: onAskAssistant,
),
const SizedBox(height: StudyOsSpacing.xxl),
_SectionLabel(label: 'StudyOS'),
const SizedBox(height: StudyOsSpacing.sm),
Expand Down
Loading
Loading