API Gemini AI Testing

This commit is contained in:
202310715297 RAIHAN ARIQ MUZAKKI 2025-11-14 14:10:13 +07:00
parent 30b9e45aa3
commit a7f770adf4
7 changed files with 426 additions and 55 deletions

View File

@ -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")
}

View File

@ -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
"""
}

View File

@ -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<GeminiResponse>
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)
}
}
}

View File

@ -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<Content>,
@SerializedName("generationConfig")
val generationConfig: GenerationConfig? = null,
@SerializedName("systemInstruction")
val systemInstruction: SystemInstruction? = null
)
data class Content(
@SerializedName("parts")
val parts: List<Part>,
@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<Part>
)
/**
* Response model dari Gemini API
*/
data class GeminiResponse(
@SerializedName("candidates")
val candidates: List<Candidate>?,
@SerializedName("error")
val error: ApiError? = null
)
data class Candidate(
@SerializedName("content")
val content: Content,
@SerializedName("finishReason")
val finishReason: String?,
@SerializedName("safetyRatings")
val safetyRatings: List<SafetyRating>?
)
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
}

View File

@ -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<String> {
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<Pair<String, String>> = emptyList()
): Result<String> {
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()))
)
)
}
}

View File

@ -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) }
)
}
}
}
}

View File

@ -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<List<NotebookEntity>>(emptyList())
@ -41,6 +43,10 @@ class NotebookViewModel(application: Application) : AndroidViewModel(application
private val _isLoading = MutableStateFlow(false)
val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()
// State error
private val _errorMessage = MutableStateFlow<String?>(null)
val errorMessage: StateFlow<String?> = _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 {
_isLoading.value = true
try {
repository.sendMessage(notebookId, message, isUserMessage = true)
// TODO: Panggil Gemini API di sini
// Sementara kirim dummy AI response
simulateAIResponse(notebookId, message)
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
}
/**
* 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\""
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
}
}
}
/**
* 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
}
}