From 3dec997d416cc3b5d2935fa3bc5cd02a1dd93539 Mon Sep 17 00:00:00 2001 From: Raihan Ariq <202310715297@mhs.ubharajaya.ac.id> Date: Thu, 13 Nov 2025 11:19:08 +0700 Subject: [PATCH 1/4] MainActivity Test --- .../java/com/example/notebook/MainActivity.kt | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/example/notebook/MainActivity.kt b/app/src/main/java/com/example/notebook/MainActivity.kt index 6c2ae50..9450b58 100644 --- a/app/src/main/java/com/example/notebook/MainActivity.kt +++ b/app/src/main/java/com/example/notebook/MainActivity.kt @@ -149,7 +149,13 @@ fun NotebookApp(viewModel: NotebookViewModel) { } } when (selectedTabIndex) { - 0 -> StudioScreen(viewModel) + 0 -> StudioScreen( + viewModel = viewModel, + onNotebookClick = { notebookId -> + println("📱 Navigasi ke notebook ID: $notebookId") + selectedNotebookId = notebookId + } + ) 1 -> ChatScreen(viewModel) 2 -> SourcesScreen(viewModel) } @@ -159,7 +165,7 @@ fun NotebookApp(viewModel: NotebookViewModel) { // === STUDIO SCREEN (UPDATED) === @Composable -fun StudioScreen(viewModel: NotebookViewModel) { +fun StudioScreen(viewModel: NotebookViewModel, onNotebookClick: (Int) -> Unit) { val notebooks by viewModel.notebooks.collectAsState() var showCreateDialog by remember { mutableStateOf(false) } @@ -199,8 +205,14 @@ fun StudioScreen(viewModel: NotebookViewModel) { items(notebooks) { notebook -> NotebookCard( notebook = notebook, - onClick = { /* TODO: Buka notebook */ }, - onDelete = { viewModel.deleteNotebook(notebook) } + onClick = { + println("🟢 onClick triggered untuk notebook ID: ${notebook.id}") + onNotebookClick(notebook.id) + }, + onDelete = { + println("🔴 Delete triggered untuk notebook ID: ${notebook.id}") + viewModel.deleteNotebook(notebook) + } ) } } @@ -248,7 +260,10 @@ fun NotebookCard( Card( modifier = Modifier .fillMaxWidth() - .clickable(onClick = onClick), + .clickable { + println("🔵 Notebook diklik: ID=${notebook.id}, Title=${notebook.title}") + onClick() + }, shape = RoundedCornerShape(12.dp), colors = CardDefaults.cardColors(containerColor = Color(0xFFF8F9FA)) ) { From 30b9e45aa3cf504f619ea5443e1bb9cd7d23772c Mon Sep 17 00:00:00 2001 From: Raihan Ariq <202310715297@mhs.ubharajaya.ac.id> Date: Thu, 13 Nov 2025 11:33:40 +0700 Subject: [PATCH 2/4] Debug Navigation --- .../java/com/example/notebook/MainActivity.kt | 24 ++++++++++++------- .../ui/screen/NotebookDetailScreen.kt | 10 ++++++++ .../notebook/viewmodel/NotebookViewModel.kt | 4 ++++ 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/example/notebook/MainActivity.kt b/app/src/main/java/com/example/notebook/MainActivity.kt index 9450b58..d12da38 100644 --- a/app/src/main/java/com/example/notebook/MainActivity.kt +++ b/app/src/main/java/com/example/notebook/MainActivity.kt @@ -64,12 +64,21 @@ fun NotebookApp(viewModel: NotebookViewModel) { var chatInput by remember { mutableStateOf("") } var selectedNotebookId by remember { mutableIntStateOf(-1) } + // Log setiap kali selectedNotebookId berubah + LaunchedEffect(selectedNotebookId) { + println("🎯 selectedNotebookId berubah menjadi: $selectedNotebookId") + } + // Kalau ada notebook yang dipilih, tampilkan detail screen if (selectedNotebookId != -1) { + println("✨ Menampilkan NotebookDetailScreen untuk ID: $selectedNotebookId") com.example.notebook.ui.screens.NotebookDetailScreen( viewModel = viewModel, notebookId = selectedNotebookId, - onBack = { selectedNotebookId = -1 } + onBack = { + println("⬅️ Kembali dari detail screen") + selectedNotebookId = -1 + } ) return } @@ -258,14 +267,13 @@ fun NotebookCard( val dateFormat = SimpleDateFormat("dd MMM yyyy, HH:mm", Locale.getDefault()) Card( - modifier = Modifier - .fillMaxWidth() - .clickable { - println("🔵 Notebook diklik: ID=${notebook.id}, Title=${notebook.title}") - onClick() - }, + modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(12.dp), - colors = CardDefaults.cardColors(containerColor = Color(0xFFF8F9FA)) + colors = CardDefaults.cardColors(containerColor = Color(0xFFF8F9FA)), + onClick = { + println("🔵 Card onClick: ID=${notebook.id}, Title=${notebook.title}") + onClick() + } ) { Row( modifier = Modifier 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..7fe1710 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( 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..f26aa7d 100644 --- a/app/src/main/java/com/example/notebook/viewmodel/NotebookViewModel.kt +++ b/app/src/main/java/com/example/notebook/viewmodel/NotebookViewModel.kt @@ -82,11 +82,15 @@ 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) } } From a7f770adf4eeef6994020dfeef8a69081d065a5b Mon Sep 17 00:00:00 2001 From: Raihan Ariq <202310715297@mhs.ubharajaya.ac.id> Date: Fri, 14 Nov 2025 14:10:13 +0700 Subject: [PATCH 3/4] API Gemini AI Testing --- app/build.gradle.kts | 11 ++ .../com/example/notebook/api/ApiConstants.kt | 30 +++ .../example/notebook/api/GeminiApiService.kt | 48 +++++ .../com/example/notebook/api/GeminiModels.kt | 94 ++++++++++ .../example/notebook/api/GeminiRepository.kt | 112 +++++++++++ .../ui/screen/NotebookDetailScreen.kt | 12 ++ .../notebook/viewmodel/NotebookViewModel.kt | 174 ++++++++++++------ 7 files changed, 426 insertions(+), 55 deletions(-) create mode 100644 app/src/main/java/com/example/notebook/api/ApiConstants.kt create mode 100644 app/src/main/java/com/example/notebook/api/GeminiApiService.kt create mode 100644 app/src/main/java/com/example/notebook/api/GeminiModels.kt create mode 100644 app/src/main/java/com/example/notebook/api/GeminiRepository.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ec57099..30b5cf0 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -80,4 +80,15 @@ 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") } \ 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..1e57b01 --- /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-1.5-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 7fe1710..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 @@ -114,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/viewmodel/NotebookViewModel.kt b/app/src/main/java/com/example/notebook/viewmodel/NotebookViewModel.kt index f26aa7d..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,9 +77,6 @@ class NotebookViewModel(application: Application) : AndroidViewModel(application } } - /** - * Pilih notebook untuk dibuka - */ fun selectNotebook(notebookId: Int) { println("📂 selectNotebook dipanggil dengan ID: $notebookId") viewModelScope.launch { @@ -97,18 +93,12 @@ class NotebookViewModel(application: Application) : AndroidViewModel(application } } - /** - * Update notebook - */ fun updateNotebook(notebook: NotebookEntity) { viewModelScope.launch { repository.updateNotebook(notebook) } } - /** - * Hapus notebook - */ fun deleteNotebook(notebook: NotebookEntity) { viewModelScope.launch { repository.deleteNotebook(notebook) @@ -117,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 -> @@ -128,9 +115,6 @@ class NotebookViewModel(application: Application) : AndroidViewModel(application } } - /** - * Tambah source baru - */ fun addSource( notebookId: Int, fileName: String, @@ -150,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 @@ -175,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) { @@ -196,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 -> @@ -207,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 From 30298ac46eb71830a59cba5ae494fb12e3c2121d Mon Sep 17 00:00:00 2001 From: Raihan Ariq <202310715297@mhs.ubharajaya.ac.id> Date: Fri, 14 Nov 2025 20:59:46 +0700 Subject: [PATCH 4/4] PDF Testing --- app/build.gradle.kts | 5 +- .../java/com/example/notebook/MainActivity.kt | 4 + .../example/notebook/api/GeminiApiService.kt | 2 +- .../com/example/notebook/utils/FileHelper.kt | 20 +++- .../com/example/notebook/utils/PdfHelper.kt | 93 +++++++++++++++++++ 5 files changed, 120 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/com/example/notebook/utils/PdfHelper.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 30b5cf0..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") @@ -91,4 +91,7 @@ dependencies { // 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 d12da38..a8f648f 100644 --- a/app/src/main/java/com/example/notebook/MainActivity.kt +++ b/app/src/main/java/com/example/notebook/MainActivity.kt @@ -40,6 +40,10 @@ class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + + // Initialize PDFBox + com.example.notebook.utils.PdfHelper.initialize(this) + setContent { NotebookTheme { Surface( diff --git a/app/src/main/java/com/example/notebook/api/GeminiApiService.kt b/app/src/main/java/com/example/notebook/api/GeminiApiService.kt index 1e57b01..824688a 100644 --- a/app/src/main/java/com/example/notebook/api/GeminiApiService.kt +++ b/app/src/main/java/com/example/notebook/api/GeminiApiService.kt @@ -15,7 +15,7 @@ import java.util.concurrent.TimeUnit */ interface GeminiApiService { - @POST("v1beta/models/gemini-1.5-flash:generateContent") + @POST("v1beta/models/gemini-2.0-flash:generateContent") suspend fun generateContent( @Query("key") apiKey: String, @Body request: GeminiRequest 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