Merge remote-tracking branch 'origin/cobaDatabase' into cobaDatabase
# Conflicts: # app/src/main/java/com/example/notebook/MainActivity.kt
This commit is contained in:
commit
2ac0393847
@ -68,7 +68,7 @@ dependencies {
|
|||||||
debugImplementation(libs.ui.tooling)
|
debugImplementation(libs.ui.tooling)
|
||||||
debugImplementation(libs.ui.test.manifest)
|
debugImplementation(libs.ui.test.manifest)
|
||||||
|
|
||||||
// Room Database - TAMBAHKAN INI SEMUA
|
// Room Database
|
||||||
val roomVersion = "2.6.1"
|
val roomVersion = "2.6.1"
|
||||||
implementation("androidx.room:room-runtime:$roomVersion")
|
implementation("androidx.room:room-runtime:$roomVersion")
|
||||||
implementation("androidx.room:room-ktx:$roomVersion")
|
implementation("androidx.room:room-ktx:$roomVersion")
|
||||||
@ -80,4 +80,18 @@ 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")
|
||||||
|
|
||||||
|
// PDF Support
|
||||||
|
implementation("com.tom-roush:pdfbox-android:2.0.27.0")
|
||||||
}
|
}
|
||||||
@ -41,18 +41,12 @@ class MainActivity : ComponentActivity() {
|
|||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContent {
|
setContent {
|
||||||
var isDarkMode by remember { mutableStateOf(false) }
|
NotebookTheme {
|
||||||
|
|
||||||
NotebookTheme(darkTheme = isDarkMode) {
|
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
color = MaterialTheme.colorScheme.background
|
color = MaterialTheme.colorScheme.background
|
||||||
) {
|
) {
|
||||||
NotebookApp(
|
NotebookApp(viewModel = viewModel)
|
||||||
viewModel = viewModel,
|
|
||||||
isDarkMode = isDarkMode,
|
|
||||||
onThemeChange = { isDarkMode = it }
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -61,13 +55,10 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun NotebookApp(
|
fun NotebookApp(viewModel: NotebookViewModel) {
|
||||||
viewModel: NotebookViewModel,
|
|
||||||
isDarkMode: Boolean,
|
|
||||||
onThemeChange: (Boolean) -> Unit
|
|
||||||
) {
|
|
||||||
var selectedTabIndex by remember { mutableIntStateOf(0) }
|
var selectedTabIndex by remember { mutableIntStateOf(0) }
|
||||||
val tabs = listOf("Studio", "Chat", "Sources")
|
val tabs = listOf("Studio", "Chat", "Sources")
|
||||||
|
var showGoogleAppsMenu by remember { mutableStateOf(false) }
|
||||||
var showSettingsMenu by remember { mutableStateOf(false) }
|
var showSettingsMenu by remember { mutableStateOf(false) }
|
||||||
var showAccountScreen by remember { mutableStateOf(false) }
|
var showAccountScreen by remember { mutableStateOf(false) }
|
||||||
var chatInput by remember { mutableStateOf("") }
|
var chatInput by remember { mutableStateOf("") }
|
||||||
@ -96,14 +87,14 @@ fun NotebookApp(
|
|||||||
IconButton(onClick = { showSettingsMenu = true }) {
|
IconButton(onClick = { showSettingsMenu = true }) {
|
||||||
Icon(Icons.Filled.Settings, contentDescription = "Settings")
|
Icon(Icons.Filled.Settings, contentDescription = "Settings")
|
||||||
}
|
}
|
||||||
SettingsMenu(
|
SettingsMenu(expanded = showSettingsMenu, onDismiss = { showSettingsMenu = false })
|
||||||
expanded = showSettingsMenu,
|
}
|
||||||
onDismiss = { showSettingsMenu = false },
|
Box {
|
||||||
isDarkMode = isDarkMode,
|
IconButton(onClick = { showGoogleAppsMenu = true }) {
|
||||||
onThemeChange = onThemeChange
|
Icon(Icons.Filled.Apps, contentDescription = "Google Apps")
|
||||||
)
|
}
|
||||||
|
GoogleAppsMenu(expanded = showGoogleAppsMenu, onDismiss = { showGoogleAppsMenu = false })
|
||||||
}
|
}
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(32.dp)
|
.size(32.dp)
|
||||||
@ -185,14 +176,14 @@ fun StudioScreen(viewModel: NotebookViewModel) {
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(MaterialTheme.colorScheme.background)
|
.background(Color.White)
|
||||||
.padding(16.dp)
|
.padding(16.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
"Notebook terbaru",
|
"Notebook terbaru",
|
||||||
style = MaterialTheme.typography.titleLarge,
|
style = MaterialTheme.typography.titleLarge,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
color = MaterialTheme.colorScheme.onBackground
|
color = Color.Black
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
@ -224,9 +215,7 @@ fun NewNotebookCard(onClick: () -> Unit) {
|
|||||||
.height(120.dp)
|
.height(120.dp)
|
||||||
.clickable(onClick = onClick),
|
.clickable(onClick = onClick),
|
||||||
shape = RoundedCornerShape(16.dp),
|
shape = RoundedCornerShape(16.dp),
|
||||||
colors = CardDefaults.cardColors(
|
colors = CardDefaults.cardColors(containerColor = Color(0xFFF0F4F7))
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceVariant
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
@ -237,20 +226,13 @@ fun NewNotebookCard(onClick: () -> Unit) {
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(40.dp)
|
.size(40.dp)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.background(MaterialTheme.colorScheme.secondaryContainer),
|
.background(Color(0xFFE1E3E6)),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(Icons.Default.Add, contentDescription = "Buat notebook baru", tint = Color.Black)
|
||||||
Icons.Default.Add,
|
|
||||||
contentDescription = "Buat notebook baru",
|
|
||||||
tint = MaterialTheme.colorScheme.onSecondaryContainer
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
Text(
|
Text("Buat notebook baru", color = Color.Black)
|
||||||
"Buat notebook baru",
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -268,9 +250,7 @@ fun NotebookCard(
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable(onClick = onClick),
|
.clickable(onClick = onClick),
|
||||||
shape = RoundedCornerShape(12.dp),
|
shape = RoundedCornerShape(12.dp),
|
||||||
colors = CardDefaults.cardColors(
|
colors = CardDefaults.cardColors(containerColor = Color(0xFFF8F9FA))
|
||||||
containerColor = MaterialTheme.colorScheme.surface
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@ -283,13 +263,13 @@ fun NotebookCard(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(48.dp)
|
.size(48.dp)
|
||||||
.clip(RoundedCornerShape(8.dp))
|
.clip(RoundedCornerShape(8.dp))
|
||||||
.background(MaterialTheme.colorScheme.primaryContainer),
|
.background(Color(0xFFE8EAF6)),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Default.Description,
|
Icons.Default.Description,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = MaterialTheme.colorScheme.onPrimaryContainer
|
tint = Color(0xFF5C6BC0)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -302,15 +282,14 @@ fun NotebookCard(
|
|||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
Text(
|
Text(
|
||||||
text = if (notebook.description.isNotBlank()) notebook.description
|
text = if (notebook.description.isNotBlank()) notebook.description
|
||||||
else "Belum ada deskripsi",
|
else "Belum ada deskripsi",
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = Color.Gray,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
@ -318,7 +297,7 @@ fun NotebookCard(
|
|||||||
Text(
|
Text(
|
||||||
text = dateFormat.format(Date(notebook.updatedAt)),
|
text = dateFormat.format(Date(notebook.updatedAt)),
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = Color.Gray
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -327,7 +306,7 @@ fun NotebookCard(
|
|||||||
Icon(
|
Icon(
|
||||||
Icons.Default.Delete,
|
Icons.Default.Delete,
|
||||||
contentDescription = "Hapus",
|
contentDescription = "Hapus",
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
tint = Color.Gray
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -405,7 +384,7 @@ fun SourcesScreen(viewModel: NotebookViewModel) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// === MENU COMPONENTS ===
|
// === MENU COMPONENTS (Tetap sama) ===
|
||||||
@Composable
|
@Composable
|
||||||
fun AccountScreen(onDismiss: () -> Unit) {
|
fun AccountScreen(onDismiss: () -> Unit) {
|
||||||
Dialog(
|
Dialog(
|
||||||
@ -456,40 +435,8 @@ fun AccountScreen(onDismiss: () -> Unit) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingsMenu(
|
fun SettingsMenu(expanded: Boolean, onDismiss: () -> Unit) {
|
||||||
expanded: Boolean,
|
|
||||||
onDismiss: () -> Unit,
|
|
||||||
isDarkMode: Boolean,
|
|
||||||
onThemeChange: (Boolean) -> Unit
|
|
||||||
) {
|
|
||||||
DropdownMenu(expanded = expanded, onDismissRequest = onDismiss) {
|
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(
|
DropdownMenuItem(
|
||||||
text = { Text("NotebookLM Help") },
|
text = { Text("NotebookLM Help") },
|
||||||
onClick = { },
|
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) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
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-2.0-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()))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -60,9 +60,19 @@ fun NotebookDetailScreen(
|
|||||||
|
|
||||||
// Load notebook data
|
// Load notebook data
|
||||||
LaunchedEffect(notebookId) {
|
LaunchedEffect(notebookId) {
|
||||||
|
println("🚀 NotebookDetailScreen: LaunchedEffect triggered untuk ID: $notebookId")
|
||||||
viewModel.selectNotebook(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(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
@ -104,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) }
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -76,11 +76,27 @@ object FileHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Baca text dari file
|
* Baca text dari file (support Text, Markdown, dan PDF)
|
||||||
*/
|
*/
|
||||||
fun readTextFromFile(filePath: String): String? {
|
fun readTextFromFile(filePath: String): String? {
|
||||||
return try {
|
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) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
null
|
null
|
||||||
|
|||||||
93
app/src/main/java/com/example/notebook/utils/PdfHelper.kt
Normal file
93
app/src/main/java/com/example/notebook/utils/PdfHelper.kt
Normal file
@ -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
|
||||||
|
)
|
||||||
@ -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,33 +77,28 @@ class NotebookViewModel(application: Application) : AndroidViewModel(application
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Pilih notebook untuk dibuka
|
|
||||||
*/
|
|
||||||
fun selectNotebook(notebookId: Int) {
|
fun selectNotebook(notebookId: Int) {
|
||||||
|
println("📂 selectNotebook dipanggil dengan ID: $notebookId")
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
repository.getNotebookById(notebookId).collect { notebook ->
|
repository.getNotebookById(notebookId).collect { notebook ->
|
||||||
|
println("📖 Notebook data: ${notebook?.title ?: "NULL"}")
|
||||||
_currentNotebook.value = notebook
|
_currentNotebook.value = notebook
|
||||||
notebook?.let {
|
notebook?.let {
|
||||||
|
println("📦 Loading sources untuk notebook ID: $notebookId")
|
||||||
loadSources(notebookId)
|
loadSources(notebookId)
|
||||||
|
println("💬 Loading chat history untuk notebook ID: $notebookId")
|
||||||
loadChatHistory(notebookId)
|
loadChatHistory(notebookId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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)
|
||||||
@ -113,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 ->
|
||||||
@ -124,9 +115,6 @@ class NotebookViewModel(application: Application) : AndroidViewModel(application
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Tambah source baru
|
|
||||||
*/
|
|
||||||
fun addSource(
|
fun addSource(
|
||||||
notebookId: Int,
|
notebookId: Int,
|
||||||
fileName: String,
|
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) {
|
fun uploadFile(context: android.content.Context, uri: android.net.Uri, notebookId: Int) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_isLoading.value = true
|
_isLoading.value = true
|
||||||
@ -171,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) {
|
||||||
@ -192,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 ->
|
||||||
@ -203,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