API Gemini AI Testing
This commit is contained in:
parent
30b9e45aa3
commit
a7f770adf4
@ -80,4 +80,15 @@ dependencies {
|
|||||||
// ViewModel & Lifecycle
|
// ViewModel & Lifecycle
|
||||||
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0")
|
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0")
|
||||||
implementation("androidx.lifecycle:lifecycle-runtime-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) }
|
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 android.app.Application
|
||||||
import androidx.lifecycle.AndroidViewModel
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.example.notebook.api.GeminiRepository
|
||||||
import com.example.notebook.data.AppDatabase
|
import com.example.notebook.data.AppDatabase
|
||||||
import com.example.notebook.data.NotebookEntity
|
import com.example.notebook.data.NotebookEntity
|
||||||
import com.example.notebook.data.NotebookRepository
|
import com.example.notebook.data.NotebookRepository
|
||||||
@ -20,6 +21,7 @@ import kotlinx.coroutines.launch
|
|||||||
class NotebookViewModel(application: Application) : AndroidViewModel(application) {
|
class NotebookViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
|
|
||||||
private val repository: NotebookRepository
|
private val repository: NotebookRepository
|
||||||
|
private val geminiRepository = GeminiRepository()
|
||||||
|
|
||||||
// State untuk list notebooks
|
// State untuk list notebooks
|
||||||
private val _notebooks = MutableStateFlow<List<NotebookEntity>>(emptyList())
|
private val _notebooks = MutableStateFlow<List<NotebookEntity>>(emptyList())
|
||||||
@ -41,6 +43,10 @@ class NotebookViewModel(application: Application) : AndroidViewModel(application
|
|||||||
private val _isLoading = MutableStateFlow(false)
|
private val _isLoading = MutableStateFlow(false)
|
||||||
val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()
|
val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()
|
||||||
|
|
||||||
|
// State error
|
||||||
|
private val _errorMessage = MutableStateFlow<String?>(null)
|
||||||
|
val errorMessage: StateFlow<String?> = _errorMessage.asStateFlow()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val database = AppDatabase.getDatabase(application)
|
val database = AppDatabase.getDatabase(application)
|
||||||
repository = NotebookRepository(database.notebookDao())
|
repository = NotebookRepository(database.notebookDao())
|
||||||
@ -49,9 +55,6 @@ class NotebookViewModel(application: Application) : AndroidViewModel(application
|
|||||||
|
|
||||||
// === NOTEBOOK FUNCTIONS ===
|
// === NOTEBOOK FUNCTIONS ===
|
||||||
|
|
||||||
/**
|
|
||||||
* Load semua notebooks dari database
|
|
||||||
*/
|
|
||||||
private fun loadNotebooks() {
|
private fun loadNotebooks() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
repository.getAllNotebooks().collect { notebooks ->
|
repository.getAllNotebooks().collect { notebooks ->
|
||||||
@ -60,15 +63,11 @@ class NotebookViewModel(application: Application) : AndroidViewModel(application
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Buat notebook baru
|
|
||||||
*/
|
|
||||||
fun createNotebook(title: String, description: String = "") {
|
fun createNotebook(title: String, description: String = "") {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_isLoading.value = true
|
_isLoading.value = true
|
||||||
try {
|
try {
|
||||||
val notebookId = repository.createNotebook(title, description)
|
val notebookId = repository.createNotebook(title, description)
|
||||||
// Otomatis ter-update karena Flow
|
|
||||||
println("✅ Notebook berhasil dibuat dengan ID: $notebookId")
|
println("✅ Notebook berhasil dibuat dengan ID: $notebookId")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
println("❌ Error membuat notebook: ${e.message}")
|
println("❌ Error membuat notebook: ${e.message}")
|
||||||
@ -78,9 +77,6 @@ class NotebookViewModel(application: Application) : AndroidViewModel(application
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Pilih notebook untuk dibuka
|
|
||||||
*/
|
|
||||||
fun selectNotebook(notebookId: Int) {
|
fun selectNotebook(notebookId: Int) {
|
||||||
println("📂 selectNotebook dipanggil dengan ID: $notebookId")
|
println("📂 selectNotebook dipanggil dengan ID: $notebookId")
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
@ -97,18 +93,12 @@ class NotebookViewModel(application: Application) : AndroidViewModel(application
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Update notebook
|
|
||||||
*/
|
|
||||||
fun updateNotebook(notebook: NotebookEntity) {
|
fun updateNotebook(notebook: NotebookEntity) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
repository.updateNotebook(notebook)
|
repository.updateNotebook(notebook)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Hapus notebook
|
|
||||||
*/
|
|
||||||
fun deleteNotebook(notebook: NotebookEntity) {
|
fun deleteNotebook(notebook: NotebookEntity) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
repository.deleteNotebook(notebook)
|
repository.deleteNotebook(notebook)
|
||||||
@ -117,9 +107,6 @@ class NotebookViewModel(application: Application) : AndroidViewModel(application
|
|||||||
|
|
||||||
// === SOURCE FUNCTIONS ===
|
// === SOURCE FUNCTIONS ===
|
||||||
|
|
||||||
/**
|
|
||||||
* Load sources untuk notebook tertentu
|
|
||||||
*/
|
|
||||||
private fun loadSources(notebookId: Int) {
|
private fun loadSources(notebookId: Int) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
repository.getSourcesByNotebook(notebookId).collect { sources ->
|
repository.getSourcesByNotebook(notebookId).collect { sources ->
|
||||||
@ -128,9 +115,6 @@ class NotebookViewModel(application: Application) : AndroidViewModel(application
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Tambah source baru
|
|
||||||
*/
|
|
||||||
fun addSource(
|
fun addSource(
|
||||||
notebookId: Int,
|
notebookId: Int,
|
||||||
fileName: String,
|
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) {
|
fun uploadFile(context: android.content.Context, uri: android.net.Uri, notebookId: Int) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_isLoading.value = true
|
_isLoading.value = true
|
||||||
@ -175,15 +156,11 @@ class NotebookViewModel(application: Application) : AndroidViewModel(application
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
fun deleteSource(source: SourceEntity) {
|
||||||
* Hapus source
|
|
||||||
*/
|
|
||||||
fun deleteSource(source: com.example.notebook.data.SourceEntity) {
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_isLoading.value = true
|
_isLoading.value = true
|
||||||
try {
|
try {
|
||||||
repository.deleteSource(source)
|
repository.deleteSource(source)
|
||||||
// Hapus file fisik juga
|
|
||||||
com.example.notebook.utils.FileHelper.deleteFile(source.filePath)
|
com.example.notebook.utils.FileHelper.deleteFile(source.filePath)
|
||||||
println("✅ Source berhasil dihapus: ${source.fileName}")
|
println("✅ Source berhasil dihapus: ${source.fileName}")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@ -196,9 +173,6 @@ class NotebookViewModel(application: Application) : AndroidViewModel(application
|
|||||||
|
|
||||||
// === CHAT FUNCTIONS ===
|
// === CHAT FUNCTIONS ===
|
||||||
|
|
||||||
/**
|
|
||||||
* Load chat history
|
|
||||||
*/
|
|
||||||
private fun loadChatHistory(notebookId: Int) {
|
private fun loadChatHistory(notebookId: Int) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
repository.getChatHistory(notebookId).collect { messages ->
|
repository.getChatHistory(notebookId).collect { messages ->
|
||||||
@ -207,36 +181,126 @@ class NotebookViewModel(application: Application) : AndroidViewModel(application
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Kirim pesan user
|
|
||||||
*/
|
|
||||||
fun sendUserMessage(notebookId: Int, message: String) {
|
fun sendUserMessage(notebookId: Int, message: String) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
repository.sendMessage(notebookId, message, isUserMessage = true)
|
_isLoading.value = true
|
||||||
// TODO: Panggil Gemini API di sini
|
try {
|
||||||
// Sementara kirim dummy AI response
|
repository.sendMessage(notebookId, message, isUserMessage = true)
|
||||||
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
fun clearChatHistory(notebookId: Int) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
repository.clearChatHistory(notebookId)
|
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