Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
4684c3d
fix: prevent duplicate fetchAssignments on startup
HeZeBang Jul 22, 2026
c7eefda
feat: port flutter_ai element showcase as native demo page
HeZeBang Jul 22, 2026
88cb6cf
feat(ai): integrate flutter_ai library for chat UI + streaming markdown
HeZeBang Jul 25, 2026
f2772b9
feat(ai): custom auto-scroll chat with scroll-to-end button
HeZeBang Jul 25, 2026
6562b60
feat(ai): tool calling for schedule/semesters/assignments/time
HeZeBang Jul 26, 2026
9b115ca
Merge origin/dev/uniform-node into dev/ai-toolkit
HeZeBang Jul 26, 2026
25bd074
feat(ai): cache-first tools with per-semester cache and refresh param
HeZeBang Jul 26, 2026
b08d829
fix(ai): stop losing tool results across restarts; block sends mid to…
HeZeBang Jul 26, 2026
1fc019c
chore(ai): drop internal tech-atlas reference from config docs/UI
HeZeBang Jul 26, 2026
a5c8640
Merge remote-tracking branch 'origin/master' into dev/ai-toolkit
HeZeBang Jul 26, 2026
d16fc0b
Merge branch 'dev/ai-toolkit' of github.com:HeZeBang/TechPie-flutter …
HeZeBang Jul 26, 2026
00d7ca1
fix: vendor flutter_ai packages so CI/remote builds are reproducible
HeZeBang Jul 26, 2026
864bccc
style: fix analyzer lints in AI pages/tests
HeZeBang Jul 26, 2026
278ecfd
refactor: consume flutter_ai from the HeZeBang/flutter_ai fork
HeZeBang Jul 26, 2026
f40a54a
feat: gate AI demo entry behind the debug-mode setting
HeZeBang Jul 26, 2026
f28e113
feat: single-instance floating feedback banner that glides with the b…
HeZeBang Jul 26, 2026
967ea48
fix: route AI demo page snackbar through the unified feedback host
HeZeBang Jul 26, 2026
e9b4561
feat: single-instance floating feedback banner that glides with the b…
HeZeBang Jul 26, 2026
f9d8179
Merge branch 'dev/navbar-refactor' into dev/ai-toolkit
HeZeBang Jul 26, 2026
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
59 changes: 49 additions & 10 deletions lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import 'dart:async';
import 'package:desktop_webview_window/desktop_webview_window.dart'
show runWebViewTitleBarWidget;
import 'package:desktop_webview_window/desktop_webview_window.dart' show runWebViewTitleBarWidget;
import 'package:dynamic_color/dynamic_color.dart';
import 'package:flutter/material.dart';
import 'package:flutter_ai_elements/flutter_ai_elements.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:techpie/utils/platform.dart';

import 'models/third_party_account.dart';
import 'services/ai_service.dart';
import 'services/assignment_service.dart';
import 'services/auth_service.dart';
import 'services/debug_logger.dart';
Expand All @@ -21,6 +22,7 @@ import 'services/third_party_auth_service.dart';
import 'services/uni_auth_service.dart';
import 'widgets/adaptive_feedback.dart';
import 'widgets/app_shell/app_shell.dart';

void main(List<String> args) async {
// If this Flutter engine is a desktop_webview_window title bar (secondary
// engine inside the webview popup), render the navigation controls and
Expand Down Expand Up @@ -79,6 +81,12 @@ Future<void> _realMain(SharedPreferences prefs) async {
scheduleService,
);
final syncService = SyncService(authService, thirdPartyAuthService, storageService);
final aiService = AiService(
storageService,
scheduleService,
assignmentService,
thirdPartyAuthService,
);

authService.onLogout = () async {
// Third-party bindings persist across logouts — they will be used by the
Expand Down Expand Up @@ -116,6 +124,7 @@ Future<void> _realMain(SharedPreferences prefs) async {
await syncService.loadCachedKey();
assignmentService.loadCached();
await scheduleService.loadCachedData();
await aiService.initialize();

runApp(
TechPieApp(
Expand All @@ -129,6 +138,7 @@ Future<void> _realMain(SharedPreferences prefs) async {
oaGymService: oaGymService,
uniAuthService: uniAuthService,
syncService: syncService,
aiService: aiService,
),
);

Expand All @@ -141,9 +151,7 @@ Future<void> _realMain(SharedPreferences prefs) async {
// refresh token (legacy session) this is a no-op and returns false —
// that is NOT a "login expired" condition, only an actual renewal
// failure is.
final renewMain = authService.isLoggedIn
? authService.tryRenewSession()
: Future.value(true);
final renewMain = authService.isLoggedIn ? authService.tryRenewSession() : Future.value(true);
final renewThirdParty = thirdPartyAuthService.autoRenewIfNeeded();

final results = await Future.wait([renewMain, renewThirdParty]);
Expand All @@ -152,9 +160,7 @@ Future<void> _realMain(SharedPreferences prefs) async {

// Only surface a renewal failure when we actually had a refresh token
// to try (a no-op returning false is not an expiry).
if (!mainOk &&
authService.session?.geekpieRefreshToken != null &&
!isIos()) {
if (!mainOk && authService.session?.geekpieRefreshToken != null && !isIos()) {
showAdaptiveFeedback(
message: '登录已过期,请重新登录',
style: AdaptiveFeedbackStyle.error,
Expand Down Expand Up @@ -234,6 +240,7 @@ class TechPieApp extends StatefulWidget {
final OaGymService oaGymService;
final UniAuthService uniAuthService;
final SyncService syncService;
final AiService aiService;

const TechPieApp({
super.key,
Expand All @@ -247,6 +254,7 @@ class TechPieApp extends StatefulWidget {
required this.oaGymService,
required this.uniAuthService,
required this.syncService,
required this.aiService,
});

@override
Expand Down Expand Up @@ -280,15 +288,46 @@ class _TechPieAppState extends State<TechPieApp> {
oaGymService: widget.oaGymService,
uniAuthService: widget.uniAuthService,
syncService: widget.syncService,
aiService: widget.aiService,
child: MaterialApp(
scaffoldMessengerKey: rootMessengerKey,
navigatorObservers: [FeedbackRouteObserver()],
builder: (context, child) => AdaptiveFeedbackHost(
key: adaptiveFeedbackHostKey,
child: child ?? const SizedBox.shrink(),
),
title: 'TechPie',
theme: widget.themeService.lightTheme,
darkTheme: widget.themeService.darkTheme,
theme: _withAiExtension(widget.themeService.lightTheme),
darkTheme: _withAiExtension(widget.themeService.darkTheme),
themeMode: widget.themeService.themeMode,
home: const AppShell(),
),
),
);
}
}

/// Registers an [AiThemeExtension] on the TechPie theme so the flutter_ai
/// widgets (AiResponse markdown, AiCodeBlock, etc.) pick up the active color
/// scheme instead of their built-in defaults. Without this they fall back to
/// `AiThemeExtension.fallback()`, whose dark code-block background clashes
/// with TechPie's surfaces.
ThemeData _withAiExtension(ThemeData base) {
final cs = base.colorScheme;
final isDark = base.brightness == Brightness.dark;
final ai = (isDark ? AiThemeExtension.dark() : AiThemeExtension.fallback())
.copyWith(
// Prose follows the surface text color; code block sits on a surface
// container tint with a contrasting foreground.
assistantTextColor: cs.onSurface,
userTextColor: cs.onSurface,
codeBackgroundColor:
isDark ? cs.surfaceContainerHighest : const Color(0xFF1E1E1E),
codeForegroundColor:
isDark ? cs.onSurface : const Color(0xFFE6E6E6),
linkColor: cs.primary,
accentColor: cs.primary,
onAccentColor: cs.onPrimary,
);
return base.copyWith(extensions: [...base.extensions.values, ai]);
}
Loading
Loading