API Gemini AI Testing
This commit is contained in:
parent
30b9e45aa3
commit
a7f770adf4
@ -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")
|
||||
}
|
||||
30
app/src/main/java/com/example/notebook/api/ApiConstants.kt
Normal file
30
app/src/main/java/com/example/notebook/api/ApiConstants.kt
Normal 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
|
||||
"""
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
94
app/src/main/java/com/example/notebook/api/GeminiModels.kt
Normal file
94
app/src/main/java/com/example/notebook/api/GeminiModels.kt
Normal 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
|
||||
}
|
||||
112
app/src/main/java/com/example/notebook/api/GeminiRepository.kt
Normal file
112
app/src/main/java/com/example/notebook/api/GeminiRepository.kt
Normal 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()))
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -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) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user