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
3 changes: 3 additions & 0 deletions devtools_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:
50 changes: 24 additions & 26 deletions ios/Flutter/AppFrameworkInfo.plist
Original file line number Diff line number Diff line change
@@ -1,26 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>13.0</string>
</dict>
</plist>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
</dict>
</plist>
5 changes: 4 additions & 1 deletion lib/app/router/app_router.dart
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,10 @@ GoRouter goRouter(Ref ref) {
GoRoute(
path: 'chat',
parentNavigatorKey: _rootNavigatorKey,
builder: (context, state) => const ChatScreen(),
builder: (context, state) {
final initialPrompt = state.extra as String?;
return ChatScreen(initialPrompt: initialPrompt);
},
),
],
),
Expand Down
29 changes: 28 additions & 1 deletion lib/features/chat/presentation/chat_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import 'package:learning_coach/shared/data/providers.dart';
import 'package:learning_coach/shared/models/models.dart';

class ChatScreen extends ConsumerStatefulWidget {
const ChatScreen({super.key});
final String? initialPrompt;

const ChatScreen({super.key, this.initialPrompt});

@override
ConsumerState<ChatScreen> createState() => _ChatScreenState();
Expand All @@ -17,6 +19,31 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
final ScrollController _scrollController = ScrollController();
bool _isSending = false;

@override
void initState() {
super.initState();
if (widget.initialPrompt != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_sendInitialMessage(widget.initialPrompt!);
});
}
}

Future<void> _sendInitialMessage(String text) async {
setState(() => _isSending = true);
try {
// Add user message first (optional, maybe we don't want to show the long prompt?)
// For now let's show it so user knows context is sent.
// Alternatively, we could send it as a "system" message or just directly call provider.
await ref.read(chatMessagesProvider.notifier).sendMessage(text);
Future.delayed(const Duration(milliseconds: 100), _scrollToBottom);
} finally {
if (mounted) {
setState(() => _isSending = false);
}
}
}

@override
void dispose() {
_controller.dispose();
Expand Down
128 changes: 122 additions & 6 deletions lib/features/home/presentation/widgets/home_widgets.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:learning_coach/core/constants/app_strings.dart';
import 'package:learning_coach/core/providers/locale_provider.dart';
import 'package:learning_coach/shared/data/api_stats_repository.dart';
import 'package:learning_coach/shared/data/providers.dart';
import 'package:learning_coach/shared/models/models.dart';

// --- Today Plan Card ---
class TodayPlanCard extends ConsumerStatefulWidget {
Expand Down Expand Up @@ -545,6 +547,7 @@ class _CoachTipCardState extends ConsumerState<CoachTipCard>
with SingleTickerProviderStateMixin {
late AnimationController _floatController;
late Animation<double> _floatAnimation;
bool _isLoading = false;

@override
void initState() {
Expand All @@ -564,6 +567,110 @@ class _CoachTipCardState extends ConsumerState<CoachTipCard>
super.dispose();
}

Future<void> _handleTap() async {
if (_isLoading) return;

setState(() => _isLoading = true);

try {
// 1. Fetch Data
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);

final dailyStatsList = await ref.read(dailyStatsProvider.future);
final sessions = await ref
.read(apiStudySessionRepositoryProvider)
.getSessions();
final goals = await ref.read(goalsProvider.future);
final userStats = ref.read(userStatsProvider);

// 2. Filter & Aggregate
final todayStats = dailyStatsList.firstWhere(
(s) => s.date == today.toIso8601String().split('T')[0],
orElse: () => DailyStats(date: '', minutes: 0, sessions: 0),
);

final todaySessions = sessions.where((s) {
final date = DateTime(
s.startTime.year,
s.startTime.month,
s.startTime.day,
);
return date.isAtSameMomentAs(today);
}).toList();

// 3. Construct Detailed Prompt
final buffer = StringBuffer();
buffer.writeln(
'Merhaba Koç, benim "Learning Coach" asistanımsın. İşte bugünkü durumum:',
);

buffer.writeln('\n👤 **Kullanıcı Profili:**');
buffer.writeln(
'- Seviye: ${userStats.level} (${userStats.stage.name.toUpperCase()})',
);
buffer.writeln(
'- XP: ${userStats.xp} / ${userStats.xpRequiredForNextLevel}',
);
buffer.writeln('- Toplam Altın: ${userStats.gold}');

buffer.writeln('\n📅 **Bugünkü Özet:**');
buffer.writeln('- Toplam Çalışma: ${todayStats.minutes} dakika');
buffer.writeln('- Oturum Sayısı: ${todayStats.sessions}');

if (todaySessions.isNotEmpty) {
buffer.writeln('\n📝 **Oturum Detayları:**');
for (final session in todaySessions) {
final goal = goals.firstWhere(
(g) => g.id == session.goalId,
orElse: () => Goal(title: 'Bilinmeyen Hedef', description: ''),
);

final timeStr =
"${session.startTime.hour.toString().padLeft(2, '0')}:${session.startTime.minute.toString().padLeft(2, '0')}";

buffer.write('- **$timeStr** | ${goal.title}');
buffer.write(' (${session.durationMinutes} dk)');

if (session.quizScore != null) {
buffer.write(' | Quiz Başarısı: %${session.quizScore}');
}

// Efficiency Check (if actual duration is tracked)
if (session.actualDurationSeconds != null) {
final actualMins = (session.actualDurationSeconds! / 60).round();
if (actualMins < session.durationMinutes) {
buffer.write(' | ⚡ Verimli (Erken bitti)');
} else if (actualMins > session.durationMinutes + 5) {
buffer.write(' | 🐢 Biraz uzadı');
}
}
buffer.writeln();
}
} else {
buffer.writeln('\nHenüz detaylı bir çalışma kaydım yok.');
}

buffer.writeln(
'\nLütfen bu verilere dayanarak bana özel, motive edici ve gelişim odaklı bir tavsiye ver. Eğer verimsiz geçtiyse nazikçe uyar, iyiyse kutla.',
);

if (!mounted) return;

// 4. Navigate
context.push('/home/chat', extra: buffer.toString());
} catch (e) {
debugPrint('Error preparing coach tip: $e');
if (mounted) {
context.push('/home/chat'); // Fallback to empty chat
}
} finally {
if (mounted) {
setState(() => _isLoading = false);
}
}
}

@override
Widget build(BuildContext context) {
final locale = ref.watch(localeProvider);
Expand Down Expand Up @@ -606,7 +713,7 @@ class _CoachTipCardState extends ConsumerState<CoachTipCard>
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () => context.push('/home/chat'),
onTap: _isLoading ? null : _handleTap,
borderRadius: BorderRadius.circular(28),
child: Padding(
padding: const EdgeInsets.all(24.0),
Expand Down Expand Up @@ -666,11 +773,20 @@ class _CoachTipCardState extends ConsumerState<CoachTipCard>
color: Colors.white.withOpacity(0.2),
shape: BoxShape.circle,
),
child: const Icon(
Icons.auto_awesome_rounded,
color: Colors.white,
size: 16,
),
child: _isLoading
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: const Icon(
Icons.auto_awesome_rounded,
color: Colors.white,
size: 16,
),
),
],
),
Expand Down
5 changes: 5 additions & 0 deletions lib/shared/services/api_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,11 @@ class ApiService {
static String get baseUrlLLM {
// 1. Önce .env dosyasında tanımlı mı diye bakıyoruz
final envUrl = dotenv.env['LLM_BASE_URL'];
final debugModeLLM = dotenv.env['DEBUG_MODE_LLM'];

if (debugModeLLM == 'true') {
return 'http://127.0.0.1:8000';
}

if (envUrl != null && envUrl.isNotEmpty) {
return envUrl;
Expand Down
Empty file added llm_backend/app/__init__.py
Empty file.
Empty file added llm_backend/app/api/__init__.py
Empty file.
18 changes: 17 additions & 1 deletion llm_backend/app/api/chat.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import time
from fastapi import APIRouter, HTTPException
from app.models.chat_models import ChatRequest, ChatResponse
from app.services.llm_service import ask_llama
Expand All @@ -7,10 +8,25 @@

@router.post("", response_model=ChatResponse)
def chat(req: ChatRequest):
start_time = time.time()
try:
logger.info(f"Kullanıcı mesajı: {req.message}")
# Determine chat type based on message content
chat_type = "COACH_TIP" if "Merhaba Koç, benim \"Learning Coach\" asistanımsın" in req.message else "GENERAL_CHAT"

logger.info(f"[{chat_type}] REQUEST: {req.message[:50]}...")

answer = ask_llama(req.message)

duration_ms = (time.time() - start_time) * 1000
logger.info(f"[{chat_type}] RESPONSE ({duration_ms:.2f}ms): {answer[:50]}...")

# Explicitly log specifically for the test requirement
logger.info(f"TEST_LOG | {chat_type} | {duration_ms:.2f}ms | Q: {req.message[:100]}... | A: {answer[:100]}...")

return ChatResponse(answer=answer)
except Exception as e:
logger.error(f"Chat error: {str(e)}")
raise HTTPException(status_code=500, detail="LLM error")

except Exception as e:
logger.error(str(e))
Expand Down
Empty file.
19 changes: 18 additions & 1 deletion llm_backend/app/core/logger.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,25 @@
import logging
import os
from datetime import datetime

# Logs directory setup
LOG_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "logs")
os.makedirs(LOG_DIR, exist_ok=True)

# Generate a unique filename for this session
session_timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
log_filename = f"chat_log_{session_timestamp}.txt"
log_filepath = os.path.join(LOG_DIR, log_filename)

# Configure logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s | %(levelname)s | %(message)s"
format="%(asctime)s | %(levelname)s | %(message)s",
handlers=[
logging.StreamHandler(), # Console output
logging.FileHandler(log_filepath, encoding='utf-8') # File output
]
)

logger = logging.getLogger("AI-COACH")
logger.info(f"Logging started. Log file: {log_filepath}")
Empty file.
Empty file.
1 change: 1 addition & 0 deletions llm_backend/logs/chat_log_20260216_020724.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
2026-02-16 02:07:24,308 | INFO | Logging started. Log file: /Users/utar/Documents/GitHub/LearningCoachApp/llm_backend/logs/chat_log_20260216_020724.txt
7 changes: 7 additions & 0 deletions llm_backend/logs/chat_log_20260216_020739.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
2026-02-16 02:07:39,831 | INFO | Logging started. Log file: /Users/utar/Documents/GitHub/LearningCoachApp/llm_backend/logs/chat_log_20260216_020739.txt
2026-02-16 02:11:00,246 | INFO | [GENERAL_CHAT] REQUEST: Merhaba, nasılsın?...
2026-02-16 02:11:00,246 | INFO | LLAMA isteği gönderildi
2026-02-16 02:11:00,251 | ERROR | Chat error: HTTPConnectionPool(host='localhost', port=11434): Max retries exceeded with url: /api/chat (Caused by NewConnectionError("HTTPConnection(host='localhost', port=11434): Failed to establish a new connection: [Errno 61] Connection refused"))
2026-02-16 02:11:00,347 | INFO | [COACH_TIP] REQUEST: Merhaba Koç, benim "Learning Coach" asistanımsın. ...
2026-02-16 02:11:00,347 | INFO | LLAMA isteği gönderildi
2026-02-16 02:11:00,348 | ERROR | Chat error: HTTPConnectionPool(host='localhost', port=11434): Max retries exceeded with url: /api/chat (Caused by NewConnectionError("HTTPConnection(host='localhost', port=11434): Failed to establish a new connection: [Errno 61] Connection refused"))
5 changes: 5 additions & 0 deletions llm_backend/logs/chat_log_20260216_021428.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
2026-02-16 02:14:28,764 | INFO | Logging started. Log file: /Users/utar/Documents/GitHub/LearningCoachApp/llm_backend/logs/chat_log_20260216_021428.txt
2026-02-16 02:14:44,040 | INFO | [GENERAL_CHAT] REQUEST: Merhaba, nasılsın?...
2026-02-16 02:14:44,040 | INFO | LLAMA isteği gönderildi
2026-02-16 02:14:45,292 | INFO | [GENERAL_CHAT] RESPONSE (1251.70ms): Nasım iyi. Ne soruyor musun?...
2026-02-16 02:14:45,292 | INFO | TEST_LOG | GENERAL_CHAT | 1251.70ms | Q: Merhaba, nasılsın?... | A: Nasım iyi. Ne soruyor musun?...
1 change: 1 addition & 0 deletions llm_backend/logs/chat_log_20260216_021720.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
2026-02-16 02:17:20,330 | INFO | Logging started. Log file: /Users/utar/Documents/GitHub/LearningCoachApp/llm_backend/logs/chat_log_20260216_021720.txt
Loading
Loading