diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ec57099..dcf0c60 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -68,7 +68,7 @@ dependencies { debugImplementation(libs.ui.tooling) debugImplementation(libs.ui.test.manifest) - // Room Database - TAMBAHKAN INI SEMUA + // Room Database val roomVersion = "2.6.1" implementation("androidx.room:room-runtime:$roomVersion") implementation("androidx.room:room-ktx:$roomVersion") @@ -80,4 +80,18 @@ dependencies { // ViewModel & Lifecycle implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0") implementation("androidx.lifecycle:lifecycle-runtime-compose:2.7.0") + + // Retrofit untuk HTTP requests + implementation("com.squareup.retrofit2:retrofit:2.9.0") + implementation("com.squareup.retrofit2:converter-gson:2.9.0") + + // OkHttp untuk logging + implementation("com.squareup.okhttp3:okhttp:4.12.0") + implementation("com.squareup.okhttp3:logging-interceptor:4.12.0") + + // Gson + implementation("com.google.code.gson:gson:2.10.1") + + // PDF Support + implementation("com.tom-roush:pdfbox-android:2.0.27.0") } \ No newline at end of file diff --git a/app/src/main/java/com/example/notebook/MainActivity.kt b/app/src/main/java/com/example/notebook/MainActivity.kt index 979c601..6c2ae50 100644 --- a/app/src/main/java/com/example/notebook/MainActivity.kt +++ b/app/src/main/java/com/example/notebook/MainActivity.kt @@ -41,18 +41,12 @@ class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { - var isDarkMode by remember { mutableStateOf(false) } - - NotebookTheme(darkTheme = isDarkMode) { + NotebookTheme { Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background ) { - NotebookApp( - viewModel = viewModel, - isDarkMode = isDarkMode, - onThemeChange = { isDarkMode = it } - ) + NotebookApp(viewModel = viewModel) } } } @@ -61,13 +55,10 @@ class MainActivity : ComponentActivity() { @OptIn(ExperimentalMaterial3Api::class) @Composable -fun NotebookApp( - viewModel: NotebookViewModel, - isDarkMode: Boolean, - onThemeChange: (Boolean) -> Unit -) { +fun NotebookApp(viewModel: NotebookViewModel) { var selectedTabIndex by remember { mutableIntStateOf(0) } val tabs = listOf("Studio", "Chat", "Sources") + var showGoogleAppsMenu by remember { mutableStateOf(false) } var showSettingsMenu by remember { mutableStateOf(false) } var showAccountScreen by remember { mutableStateOf(false) } var chatInput by remember { mutableStateOf("") } @@ -96,14 +87,14 @@ fun NotebookApp( IconButton(onClick = { showSettingsMenu = true }) { Icon(Icons.Filled.Settings, contentDescription = "Settings") } - SettingsMenu( - expanded = showSettingsMenu, - onDismiss = { showSettingsMenu = false }, - isDarkMode = isDarkMode, - onThemeChange = onThemeChange - ) + SettingsMenu(expanded = showSettingsMenu, onDismiss = { showSettingsMenu = false }) + } + Box { + IconButton(onClick = { showGoogleAppsMenu = true }) { + Icon(Icons.Filled.Apps, contentDescription = "Google Apps") + } + GoogleAppsMenu(expanded = showGoogleAppsMenu, onDismiss = { showGoogleAppsMenu = false }) } - Box( modifier = Modifier .size(32.dp) @@ -185,14 +176,14 @@ fun StudioScreen(viewModel: NotebookViewModel) { Column( modifier = Modifier .fillMaxSize() - .background(MaterialTheme.colorScheme.background) + .background(Color.White) .padding(16.dp) ) { Text( "Notebook terbaru", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.onBackground + color = Color.Black ) Spacer(modifier = Modifier.height(16.dp)) @@ -224,9 +215,7 @@ fun NewNotebookCard(onClick: () -> Unit) { .height(120.dp) .clickable(onClick = onClick), shape = RoundedCornerShape(16.dp), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant - ) + colors = CardDefaults.cardColors(containerColor = Color(0xFFF0F4F7)) ) { Column( modifier = Modifier.fillMaxSize(), @@ -237,20 +226,13 @@ fun NewNotebookCard(onClick: () -> Unit) { modifier = Modifier .size(40.dp) .clip(CircleShape) - .background(MaterialTheme.colorScheme.secondaryContainer), + .background(Color(0xFFE1E3E6)), contentAlignment = Alignment.Center ) { - Icon( - Icons.Default.Add, - contentDescription = "Buat notebook baru", - tint = MaterialTheme.colorScheme.onSecondaryContainer - ) + Icon(Icons.Default.Add, contentDescription = "Buat notebook baru", tint = Color.Black) } Spacer(modifier = Modifier.height(8.dp)) - Text( - "Buat notebook baru", - color = MaterialTheme.colorScheme.onSurfaceVariant - ) + Text("Buat notebook baru", color = Color.Black) } } } @@ -268,9 +250,7 @@ fun NotebookCard( .fillMaxWidth() .clickable(onClick = onClick), shape = RoundedCornerShape(12.dp), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surface - ) + colors = CardDefaults.cardColors(containerColor = Color(0xFFF8F9FA)) ) { Row( modifier = Modifier @@ -283,13 +263,13 @@ fun NotebookCard( modifier = Modifier .size(48.dp) .clip(RoundedCornerShape(8.dp)) - .background(MaterialTheme.colorScheme.primaryContainer), + .background(Color(0xFFE8EAF6)), contentAlignment = Alignment.Center ) { Icon( Icons.Default.Description, contentDescription = null, - tint = MaterialTheme.colorScheme.onPrimaryContainer + tint = Color(0xFF5C6BC0) ) } @@ -302,15 +282,14 @@ fun NotebookCard( style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, maxLines = 1, - overflow = TextOverflow.Ellipsis, - color = MaterialTheme.colorScheme.onSurface + overflow = TextOverflow.Ellipsis ) Spacer(modifier = Modifier.height(4.dp)) Text( text = if (notebook.description.isNotBlank()) notebook.description else "Belum ada deskripsi", style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, + color = Color.Gray, maxLines = 1, overflow = TextOverflow.Ellipsis ) @@ -318,7 +297,7 @@ fun NotebookCard( Text( text = dateFormat.format(Date(notebook.updatedAt)), style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant + color = Color.Gray ) } @@ -327,7 +306,7 @@ fun NotebookCard( Icon( Icons.Default.Delete, contentDescription = "Hapus", - tint = MaterialTheme.colorScheme.onSurfaceVariant + tint = Color.Gray ) } } @@ -405,7 +384,7 @@ fun SourcesScreen(viewModel: NotebookViewModel) { } } -// === MENU COMPONENTS === +// === MENU COMPONENTS (Tetap sama) === @Composable fun AccountScreen(onDismiss: () -> Unit) { Dialog( @@ -456,40 +435,8 @@ fun AccountScreen(onDismiss: () -> Unit) { } @Composable -fun SettingsMenu( - expanded: Boolean, - onDismiss: () -> Unit, - isDarkMode: Boolean, - onThemeChange: (Boolean) -> Unit -) { +fun SettingsMenu(expanded: Boolean, onDismiss: () -> Unit) { DropdownMenu(expanded = expanded, onDismissRequest = onDismiss) { - // Dark Mode Toggle - DropdownMenuItem( - text = { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Row(verticalAlignment = Alignment.CenterVertically) { - Icon( - if (isDarkMode) Icons.Default.DarkMode else Icons.Default.LightMode, - contentDescription = null - ) - Spacer(modifier = Modifier.width(12.dp)) - Text(if (isDarkMode) "Mode Gelap" else "Mode Terang") - } - Switch( - checked = isDarkMode, - onCheckedChange = onThemeChange - ) - } - }, - onClick = { onThemeChange(!isDarkMode) } - ) - - Divider() - DropdownMenuItem( text = { Text("NotebookLM Help") }, onClick = { }, @@ -498,3 +445,20 @@ fun SettingsMenu( } } +@Composable +fun GoogleAppsMenu(expanded: Boolean, onDismiss: () -> Unit) { + val apps = listOf( + "Account" to Icons.Default.AccountCircle, + "Gmail" to Icons.Default.Mail, + "Drive" to Icons.Default.Cloud + ) + DropdownMenu(expanded = expanded, onDismissRequest = onDismiss) { + apps.forEach { (name, icon) -> + DropdownMenuItem( + text = { Text(name) }, + onClick = { }, + leadingIcon = { Icon(icon, contentDescription = name) } + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/notebook/api/ApiConstants.kt b/app/src/main/java/com/example/notebook/api/ApiConstants.kt new file mode 100644 index 0000000..3fa1e29 --- /dev/null +++ b/app/src/main/java/com/example/notebook/api/ApiConstants.kt @@ -0,0 +1,30 @@ +package com.example.notebook.api + +/** + * Constants untuk Gemini API + * + * PENTING: Jangan commit API key ke Git! + * Untuk production, gunakan BuildConfig atau environment variable + */ +object ApiConstants { + + // GANTI INI DENGAN API KEY KAMU + const val GEMINI_API_KEY = "AIzaSyCVYFUMcKqCDKN5Z_vNwT2Z4VHgjJ5V7dI" + + // Endpoint Gemini API + const val BASE_URL = "https://generativelanguage.googleapis.com/" + + // Model yang digunakan (Flash = gratis & cepat) + const val MODEL_NAME = "gemini-2.0-flash" + + // System instruction untuk AI + const val SYSTEM_INSTRUCTION = """ + Kamu adalah asisten AI yang membantu pengguna memahami dokumen mereka. + Tugasmu: + 1. Membuat ringkasan yang jelas dan informatif dari dokumen + 2. Menjawab pertanyaan pengguna berdasarkan isi dokumen + 3. Selalu rujuk informasi yang ada di dokumen + 4. Jika informasi tidak ada di dokumen, katakan dengan jelas + 5. Gunakan bahasa Indonesia yang mudah dipahami + """ +} \ No newline at end of file diff --git a/app/src/main/java/com/example/notebook/api/GeminiApiService.kt b/app/src/main/java/com/example/notebook/api/GeminiApiService.kt new file mode 100644 index 0000000..824688a --- /dev/null +++ b/app/src/main/java/com/example/notebook/api/GeminiApiService.kt @@ -0,0 +1,48 @@ +package com.example.notebook.api + +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Response +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import retrofit2.http.Body +import retrofit2.http.POST +import retrofit2.http.Query +import java.util.concurrent.TimeUnit + +/** + * Retrofit interface untuk Gemini API + */ +interface GeminiApiService { + + @POST("v1beta/models/gemini-2.0-flash:generateContent") + suspend fun generateContent( + @Query("key") apiKey: String, + @Body request: GeminiRequest + ): Response + + companion object { + fun create(): GeminiApiService { + // Logger untuk debug + val logger = HttpLoggingInterceptor().apply { + level = HttpLoggingInterceptor.Level.BODY + } + + // HTTP Client + val client = OkHttpClient.Builder() + .addInterceptor(logger) + .connectTimeout(60, TimeUnit.SECONDS) + .readTimeout(60, TimeUnit.SECONDS) + .writeTimeout(60, TimeUnit.SECONDS) + .build() + + // Retrofit instance + return Retrofit.Builder() + .baseUrl(ApiConstants.BASE_URL) + .client(client) + .addConverterFactory(GsonConverterFactory.create()) + .build() + .create(GeminiApiService::class.java) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/notebook/api/GeminiModels.kt b/app/src/main/java/com/example/notebook/api/GeminiModels.kt new file mode 100644 index 0000000..cbbdc62 --- /dev/null +++ b/app/src/main/java/com/example/notebook/api/GeminiModels.kt @@ -0,0 +1,94 @@ +package com.example.notebook.api + +import com.google.gson.annotations.SerializedName + +/** + * Request model untuk Gemini API + */ +data class GeminiRequest( + @SerializedName("contents") + val contents: List, + + @SerializedName("generationConfig") + val generationConfig: GenerationConfig? = null, + + @SerializedName("systemInstruction") + val systemInstruction: SystemInstruction? = null +) + +data class Content( + @SerializedName("parts") + val parts: List, + + @SerializedName("role") + val role: String = "user" +) + +data class Part( + @SerializedName("text") + val text: String +) + +data class GenerationConfig( + @SerializedName("temperature") + val temperature: Double = 0.7, + + @SerializedName("maxOutputTokens") + val maxOutputTokens: Int = 2048, + + @SerializedName("topP") + val topP: Double = 0.95 +) + +data class SystemInstruction( + @SerializedName("parts") + val parts: List +) + +/** + * Response model dari Gemini API + */ +data class GeminiResponse( + @SerializedName("candidates") + val candidates: List?, + + @SerializedName("error") + val error: ApiError? = null +) + +data class Candidate( + @SerializedName("content") + val content: Content, + + @SerializedName("finishReason") + val finishReason: String?, + + @SerializedName("safetyRatings") + val safetyRatings: List? +) + +data class SafetyRating( + @SerializedName("category") + val category: String, + + @SerializedName("probability") + val probability: String +) + +data class ApiError( + @SerializedName("code") + val code: Int, + + @SerializedName("message") + val message: String, + + @SerializedName("status") + val status: String +) + +/** + * Helper untuk extract text dari response + */ +fun GeminiResponse.getTextResponse(): String? { + return candidates?.firstOrNull()?.content?.parts?.firstOrNull()?.text +} \ No newline at end of file diff --git a/app/src/main/java/com/example/notebook/api/GeminiRepository.kt b/app/src/main/java/com/example/notebook/api/GeminiRepository.kt new file mode 100644 index 0000000..b3cc9cb --- /dev/null +++ b/app/src/main/java/com/example/notebook/api/GeminiRepository.kt @@ -0,0 +1,112 @@ +package com.example.notebook.api + +/** + * Repository untuk handle Gemini API calls + */ +class GeminiRepository { + + private val apiService = GeminiApiService.create() + + /** + * Generate summary dari text + */ + suspend fun generateSummary(text: String): Result { + return try { + val prompt = """ + Buatlah ringkasan yang komprehensif dari dokumen berikut dalam bahasa Indonesia. + Ringkasan harus: + - Mencakup poin-poin utama + - Mudah dipahami + - Panjang sekitar 3-5 paragraf + + Dokumen: + $text + """.trimIndent() + + val request = createRequest(prompt) + val response = apiService.generateContent(ApiConstants.GEMINI_API_KEY, request) + + if (response.isSuccessful) { + val body = response.body() + val textResponse = body?.getTextResponse() + + if (textResponse != null) { + Result.success(textResponse) + } else { + Result.failure(Exception("Empty response from API")) + } + } else { + Result.failure(Exception("API Error: ${response.code()} - ${response.message()}")) + } + } catch (e: Exception) { + Result.failure(e) + } + } + + /** + * Chat dengan context dokumen + */ + suspend fun chatWithDocument( + userMessage: String, + documentContext: String, + chatHistory: List> = emptyList() + ): Result { + return try { + // Build context dengan chat history + val contextBuilder = StringBuilder() + contextBuilder.append("Konteks Dokumen:\n$documentContext\n\n") + + if (chatHistory.isNotEmpty()) { + contextBuilder.append("Riwayat Chat:\n") + chatHistory.forEach { (user, ai) -> + contextBuilder.append("User: $user\n") + contextBuilder.append("AI: $ai\n\n") + } + } + + contextBuilder.append("Pertanyaan User: $userMessage\n\n") + contextBuilder.append("Jawab berdasarkan dokumen di atas. Jika informasi tidak ada di dokumen, katakan dengan jelas.") + + val request = createRequest(contextBuilder.toString()) + val response = apiService.generateContent(ApiConstants.GEMINI_API_KEY, request) + + if (response.isSuccessful) { + val body = response.body() + val textResponse = body?.getTextResponse() + + if (textResponse != null) { + Result.success(textResponse) + } else { + Result.failure(Exception("Empty response from API")) + } + } else { + val errorBody = response.errorBody()?.string() + Result.failure(Exception("API Error: ${response.code()} - $errorBody")) + } + } catch (e: Exception) { + Result.failure(e) + } + } + + /** + * Create request object dengan system instruction + */ + private fun createRequest(prompt: String): GeminiRequest { + return GeminiRequest( + contents = listOf( + Content( + parts = listOf(Part(prompt)), + role = "user" + ) + ), + generationConfig = GenerationConfig( + temperature = 0.7, + maxOutputTokens = 2048, + topP = 0.95 + ), + systemInstruction = SystemInstruction( + parts = listOf(Part(ApiConstants.SYSTEM_INSTRUCTION.trimIndent())) + ) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/notebook/ui/screen/NotebookDetailScreen.kt b/app/src/main/java/com/example/notebook/ui/screen/NotebookDetailScreen.kt index ca16f13..8571e7a 100644 --- a/app/src/main/java/com/example/notebook/ui/screen/NotebookDetailScreen.kt +++ b/app/src/main/java/com/example/notebook/ui/screen/NotebookDetailScreen.kt @@ -60,9 +60,19 @@ fun NotebookDetailScreen( // Load notebook data LaunchedEffect(notebookId) { + println("🚀 NotebookDetailScreen: LaunchedEffect triggered untuk ID: $notebookId") viewModel.selectNotebook(notebookId) } + // Debug log untuk state changes + LaunchedEffect(notebook) { + println("📝 Notebook state updated: ${notebook?.title ?: "NULL"}") + } + + LaunchedEffect(sources) { + println("📚 Sources updated: ${sources.size} items") + } + Scaffold( topBar = { TopAppBar( @@ -104,6 +114,18 @@ fun NotebookDetailScreen( }, leadingIcon = { Icon(Icons.Default.CloudUpload, null) } ) + if (sources.isNotEmpty()) { + Divider() + DropdownMenuItem( + text = { Text("Generate Summary") }, + onClick = { + viewModel.generateSummary(notebookId) + selectedTab = 0 // Switch ke chat tab + showUploadMenu = false + }, + leadingIcon = { Icon(Icons.Default.Summarize, null) } + ) + } } } } diff --git a/app/src/main/java/com/example/notebook/utils/FileHelper.kt b/app/src/main/java/com/example/notebook/utils/FileHelper.kt index 1c97023..413b657 100644 --- a/app/src/main/java/com/example/notebook/utils/FileHelper.kt +++ b/app/src/main/java/com/example/notebook/utils/FileHelper.kt @@ -76,11 +76,27 @@ object FileHelper { } /** - * Baca text dari file + * Baca text dari file (support Text, Markdown, dan PDF) */ fun readTextFromFile(filePath: String): String? { return try { - File(filePath).readText() + val file = File(filePath) + val extension = file.extension.lowercase() + + when (extension) { + "pdf" -> { + // Extract text dari PDF + PdfHelper.extractTextFromPdf(filePath) + } + "txt", "md", "markdown" -> { + // Baca text biasa + file.readText() + } + else -> { + println("⚠️ Format file tidak didukung untuk ekstraksi teks: $extension") + null + } + } } catch (e: Exception) { e.printStackTrace() null diff --git a/app/src/main/java/com/example/notebook/utils/PdfHelper.kt b/app/src/main/java/com/example/notebook/utils/PdfHelper.kt new file mode 100644 index 0000000..c2fc1bf --- /dev/null +++ b/app/src/main/java/com/example/notebook/utils/PdfHelper.kt @@ -0,0 +1,93 @@ +package com.example.notebook.utils + +import android.content.Context +import android.net.Uri +import com.tom_roush.pdfbox.android.PDFBoxResourceLoader +import com.tom_roush.pdfbox.pdmodel.PDDocument +import com.tom_roush.pdfbox.text.PDFTextStripper +import java.io.File + +/** + * Helper untuk extract text dari PDF + */ +object PdfHelper { + + /** + * Initialize PDFBox (panggil sekali saat app start) + */ + fun initialize(context: Context) { + PDFBoxResourceLoader.init(context) + } + + /** + * Extract text dari PDF file + * Return: Text content atau null jika error + */ + fun extractTextFromPdf(filePath: String): String? { + return try { + val file = File(filePath) + val document = PDDocument.load(file) + + val stripper = PDFTextStripper() + val text = stripper.getText(document) + + document.close() + + // Cleanup whitespace + text.trim() + } catch (e: Exception) { + println("❌ Error extract PDF: ${e.message}") + e.printStackTrace() + null + } + } + + /** + * Extract text dari PDF URI (sebelum disimpan) + */ + fun extractTextFromPdfUri(context: Context, uri: Uri): String? { + return try { + val inputStream = context.contentResolver.openInputStream(uri) + val document = PDDocument.load(inputStream) + + val stripper = PDFTextStripper() + val text = stripper.getText(document) + + document.close() + inputStream?.close() + + text.trim() + } catch (e: Exception) { + println("❌ Error extract PDF from URI: ${e.message}") + e.printStackTrace() + null + } + } + + /** + * Get info PDF (jumlah halaman, dll) + */ + fun getPdfInfo(filePath: String): PdfInfo? { + return try { + val file = File(filePath) + val document = PDDocument.load(file) + + val info = PdfInfo( + pageCount = document.numberOfPages, + title = document.documentInformation.title ?: "Unknown", + author = document.documentInformation.author ?: "Unknown" + ) + + document.close() + info + } catch (e: Exception) { + null + } + } +} + +data class PdfInfo( + val pageCount: Int, + val title: String, + val author: String +) \ No newline at end of file diff --git a/app/src/main/java/com/example/notebook/viewmodel/NotebookViewModel.kt b/app/src/main/java/com/example/notebook/viewmodel/NotebookViewModel.kt index e4d7075..d9ea9a1 100644 --- a/app/src/main/java/com/example/notebook/viewmodel/NotebookViewModel.kt +++ b/app/src/main/java/com/example/notebook/viewmodel/NotebookViewModel.kt @@ -3,6 +3,7 @@ package com.example.notebook.viewmodel import android.app.Application import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope +import com.example.notebook.api.GeminiRepository import com.example.notebook.data.AppDatabase import com.example.notebook.data.NotebookEntity import com.example.notebook.data.NotebookRepository @@ -20,6 +21,7 @@ import kotlinx.coroutines.launch class NotebookViewModel(application: Application) : AndroidViewModel(application) { private val repository: NotebookRepository + private val geminiRepository = GeminiRepository() // State untuk list notebooks private val _notebooks = MutableStateFlow>(emptyList()) @@ -41,6 +43,10 @@ class NotebookViewModel(application: Application) : AndroidViewModel(application private val _isLoading = MutableStateFlow(false) val isLoading: StateFlow = _isLoading.asStateFlow() + // State error + private val _errorMessage = MutableStateFlow(null) + val errorMessage: StateFlow = _errorMessage.asStateFlow() + init { val database = AppDatabase.getDatabase(application) repository = NotebookRepository(database.notebookDao()) @@ -49,9 +55,6 @@ class NotebookViewModel(application: Application) : AndroidViewModel(application // === NOTEBOOK FUNCTIONS === - /** - * Load semua notebooks dari database - */ private fun loadNotebooks() { viewModelScope.launch { repository.getAllNotebooks().collect { notebooks -> @@ -60,15 +63,11 @@ class NotebookViewModel(application: Application) : AndroidViewModel(application } } - /** - * Buat notebook baru - */ fun createNotebook(title: String, description: String = "") { viewModelScope.launch { _isLoading.value = true try { val notebookId = repository.createNotebook(title, description) - // Otomatis ter-update karena Flow println("✅ Notebook berhasil dibuat dengan ID: $notebookId") } catch (e: Exception) { println("❌ Error membuat notebook: ${e.message}") @@ -78,33 +77,28 @@ class NotebookViewModel(application: Application) : AndroidViewModel(application } } - /** - * Pilih notebook untuk dibuka - */ fun selectNotebook(notebookId: Int) { + println("📂 selectNotebook dipanggil dengan ID: $notebookId") viewModelScope.launch { repository.getNotebookById(notebookId).collect { notebook -> + println("📖 Notebook data: ${notebook?.title ?: "NULL"}") _currentNotebook.value = notebook notebook?.let { + println("📦 Loading sources untuk notebook ID: $notebookId") loadSources(notebookId) + println("💬 Loading chat history untuk notebook ID: $notebookId") loadChatHistory(notebookId) } } } } - /** - * Update notebook - */ fun updateNotebook(notebook: NotebookEntity) { viewModelScope.launch { repository.updateNotebook(notebook) } } - /** - * Hapus notebook - */ fun deleteNotebook(notebook: NotebookEntity) { viewModelScope.launch { repository.deleteNotebook(notebook) @@ -113,9 +107,6 @@ class NotebookViewModel(application: Application) : AndroidViewModel(application // === SOURCE FUNCTIONS === - /** - * Load sources untuk notebook tertentu - */ private fun loadSources(notebookId: Int) { viewModelScope.launch { repository.getSourcesByNotebook(notebookId).collect { sources -> @@ -124,9 +115,6 @@ class NotebookViewModel(application: Application) : AndroidViewModel(application } } - /** - * Tambah source baru - */ fun addSource( notebookId: Int, fileName: String, @@ -146,9 +134,6 @@ class NotebookViewModel(application: Application) : AndroidViewModel(application } } - /** - * Handle file upload dari URI - */ fun uploadFile(context: android.content.Context, uri: android.net.Uri, notebookId: Int) { viewModelScope.launch { _isLoading.value = true @@ -171,15 +156,11 @@ class NotebookViewModel(application: Application) : AndroidViewModel(application } } - /** - * Hapus source - */ - fun deleteSource(source: com.example.notebook.data.SourceEntity) { + fun deleteSource(source: SourceEntity) { viewModelScope.launch { _isLoading.value = true try { repository.deleteSource(source) - // Hapus file fisik juga com.example.notebook.utils.FileHelper.deleteFile(source.filePath) println("✅ Source berhasil dihapus: ${source.fileName}") } catch (e: Exception) { @@ -192,9 +173,6 @@ class NotebookViewModel(application: Application) : AndroidViewModel(application // === CHAT FUNCTIONS === - /** - * Load chat history - */ private fun loadChatHistory(notebookId: Int) { viewModelScope.launch { repository.getChatHistory(notebookId).collect { messages -> @@ -203,36 +181,126 @@ class NotebookViewModel(application: Application) : AndroidViewModel(application } } - /** - * Kirim pesan user - */ fun sendUserMessage(notebookId: Int, message: String) { viewModelScope.launch { - repository.sendMessage(notebookId, message, isUserMessage = true) - // TODO: Panggil Gemini API di sini - // Sementara kirim dummy AI response - simulateAIResponse(notebookId, message) + _isLoading.value = true + try { + repository.sendMessage(notebookId, message, isUserMessage = true) + + val documentContext = buildDocumentContext() + + if (documentContext.isEmpty()) { + val reply = "Maaf, belum ada dokumen yang diupload. Silakan upload dokumen terlebih dahulu untuk bisa bertanya." + repository.sendMessage(notebookId, reply, isUserMessage = false) + } else { + val chatHistory = _chatMessages.value + .takeLast(10) + .filter { it.isUserMessage } + .mapNotNull { userMsg -> + val aiMsg = _chatMessages.value.find { + !it.isUserMessage && it.timestamp > userMsg.timestamp + } + if (aiMsg != null) { + Pair(userMsg.message, aiMsg.message) + } else null + } + + val result = geminiRepository.chatWithDocument( + userMessage = message, + documentContext = documentContext, + chatHistory = chatHistory + ) + + result.fold( + onSuccess = { aiResponse -> + repository.sendMessage(notebookId, aiResponse, isUserMessage = false) + println("✅ AI response berhasil: ${aiResponse.take(50)}...") + }, + onFailure = { error -> + val errorMsg = "Maaf, terjadi error: ${error.message}" + repository.sendMessage(notebookId, errorMsg, isUserMessage = false) + println("❌ Error dari Gemini: ${error.message}") + _errorMessage.value = error.message + } + ) + } + } catch (e: Exception) { + println("❌ Error mengirim pesan: ${e.message}") + _errorMessage.value = e.message + } finally { + _isLoading.value = false + } } } - /** - * Simulasi AI response (sementara sebelum Gemini API) - */ - private fun simulateAIResponse(notebookId: Int, userMessage: String) { - viewModelScope.launch { - // Delay simulasi "AI thinking" - kotlinx.coroutines.delay(1000) - val aiResponse = "Ini adalah response sementara untuk: \"$userMessage\"" - repository.sendMessage(notebookId, aiResponse, isUserMessage = false) - } - } - - /** - * Clear chat history - */ fun clearChatHistory(notebookId: Int) { viewModelScope.launch { repository.clearChatHistory(notebookId) } } + + // === GEMINI FUNCTIONS === + + private fun buildDocumentContext(): String { + val context = StringBuilder() + + _sources.value.forEach { source -> + when (source.fileType) { + "Text", "Markdown" -> { + val content = com.example.notebook.utils.FileHelper.readTextFromFile(source.filePath) + if (content != null) { + context.append("=== ${source.fileName} ===\n") + context.append(content) + context.append("\n\n") + } + } + else -> { + context.append("=== ${source.fileName} ===\n") + context.append("[File type ${source.fileType} - content extraction not yet supported]\n\n") + } + } + } + + return context.toString() + } + + fun generateSummary(notebookId: Int) { + viewModelScope.launch { + _isLoading.value = true + try { + val documentContext = buildDocumentContext() + + if (documentContext.isEmpty()) { + _errorMessage.value = "Tidak ada dokumen untuk diringkas" + return@launch + } + + val result = geminiRepository.generateSummary(documentContext) + + result.fold( + onSuccess = { summary -> + repository.sendMessage( + notebookId, + "📝 Ringkasan Dokumen:\n\n$summary", + isUserMessage = false + ) + println("✅ Summary berhasil: ${summary.take(100)}...") + }, + onFailure = { error -> + _errorMessage.value = "Error generate summary: ${error.message}" + println("❌ Error generate summary: ${error.message}") + } + ) + } catch (e: Exception) { + _errorMessage.value = e.message + println("❌ Error: ${e.message}") + } finally { + _isLoading.value = false + } + } + } + + fun clearError() { + _errorMessage.value = null + } } \ No newline at end of file