Refinement
This commit is contained in:
parent
a51c24030f
commit
b1d99a37bb
@ -49,7 +49,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
NotebookTheme(darkTheme = isDarkMode) {
|
NotebookTheme(darkTheme = isDarkMode) {
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
color = MaterialTheme.colorScheme.background // [DIUBAH] Menggunakan warna tema
|
color = MaterialTheme.colorScheme.background
|
||||||
) {
|
) {
|
||||||
NotebookBottomSheetApp(
|
NotebookBottomSheetApp(
|
||||||
viewModel = viewModel,
|
viewModel = viewModel,
|
||||||
@ -99,7 +99,7 @@ fun NotebookBottomSheetApp(
|
|||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(MaterialTheme.colorScheme.background) // [DIUBAH] Menggunakan warna tema
|
.background(MaterialTheme.colorScheme.background)
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@ -109,7 +109,7 @@ fun NotebookBottomSheetApp(
|
|||||||
shape = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp)
|
shape = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp)
|
||||||
)
|
)
|
||||||
.clip(RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp))
|
.clip(RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp))
|
||||||
.background(MaterialTheme.colorScheme.surface) // [DIUBAH] Menggunakan warna tema
|
.background(MaterialTheme.colorScheme.surface)
|
||||||
) {
|
) {
|
||||||
NotebookHeader(
|
NotebookHeader(
|
||||||
isDarkMode = isDarkMode,
|
isDarkMode = isDarkMode,
|
||||||
@ -134,12 +134,12 @@ fun NotebookBottomSheetApp(
|
|||||||
"Notebook Saya",
|
"Notebook Saya",
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
color = MaterialTheme.colorScheme.onSurface // [DIUBAH]
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
"${notebooks.size} notebook tersimpan",
|
"${notebooks.size} notebook tersimpan",
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant // [DIUBAH]
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -208,12 +208,12 @@ fun NotebookHeader(
|
|||||||
"NoteBook",
|
"NoteBook",
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
color = MaterialTheme.colorScheme.onSurface // [DIUBAH]
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
"Selamat datang kembali!",
|
"Selamat datang kembali!",
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant, // [DIUBAH]
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
fontSize = 11.sp
|
fontSize = 11.sp
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -224,7 +224,7 @@ fun NotebookHeader(
|
|||||||
Icon(
|
Icon(
|
||||||
imageVector = if (isDarkMode) Icons.Default.LightMode else Icons.Default.DarkMode,
|
imageVector = if (isDarkMode) Icons.Default.LightMode else Icons.Default.DarkMode,
|
||||||
contentDescription = "Toggle Theme",
|
contentDescription = "Toggle Theme",
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant // [DIUBAH]
|
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -269,10 +269,10 @@ fun AccountScreen(onDismiss: () -> Unit) {
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(80.dp)
|
.size(80.dp)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.background(MaterialTheme.colorScheme.primary), // [DIUBAH]
|
.background(MaterialTheme.colorScheme.primary),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Text("A", fontSize = 40.sp, color = MaterialTheme.colorScheme.onPrimary, fontWeight = FontWeight.Bold) // [DIUBAH]
|
Text("A", fontSize = 40.sp, color = MaterialTheme.colorScheme.onPrimary, fontWeight = FontWeight.Bold)
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
Text("user@google.com", fontWeight = FontWeight.Bold, fontSize = 20.sp, color = MaterialTheme.colorScheme.onSurface)
|
Text("user@google.com", fontWeight = FontWeight.Bold, fontSize = 20.sp, color = MaterialTheme.colorScheme.onSurface)
|
||||||
@ -280,7 +280,7 @@ fun AccountScreen(onDismiss: () -> Unit) {
|
|||||||
Text("Fazri Abdurrahman", fontSize = 16.sp, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
Text("Fazri Abdurrahman", fontSize = 16.sp, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
// [DIUBAH] Tombol Kelola Akun menjadi OutlinedButton
|
// Tombol Kelola Akun menjadi OutlinedButton
|
||||||
OutlinedButton(onClick = { /* TODO: Logika Google Sign-In */ }, modifier = Modifier.fillMaxWidth()) {
|
OutlinedButton(onClick = { /* TODO: Logika Google Sign-In */ }, modifier = Modifier.fillMaxWidth()) {
|
||||||
Text("Kelola Akun Google Anda")
|
Text("Kelola Akun Google Anda")
|
||||||
}
|
}
|
||||||
@ -434,7 +434,7 @@ fun NotebookCardItem(
|
|||||||
.clickable(onClick = onClick),
|
.clickable(onClick = onClick),
|
||||||
shape = RoundedCornerShape(12.dp),
|
shape = RoundedCornerShape(12.dp),
|
||||||
colors = CardDefaults.cardColors(
|
colors = CardDefaults.cardColors(
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceContainerLowest // [DIUBAH]
|
containerColor = MaterialTheme.colorScheme.surfaceContainerLowest
|
||||||
),
|
),
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp)
|
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp)
|
||||||
) {
|
) {
|
||||||
@ -468,13 +468,13 @@ fun NotebookCardItem(
|
|||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
color = MaterialTheme.colorScheme.onSurface // [DIUBAH]
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
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, // [DIUBAH]
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
fontSize = 12.sp
|
fontSize = 12.sp
|
||||||
)
|
)
|
||||||
if (notebook.description.isNotBlank()) {
|
if (notebook.description.isNotBlank()) {
|
||||||
@ -482,7 +482,7 @@ fun NotebookCardItem(
|
|||||||
Text(
|
Text(
|
||||||
text = notebook.description,
|
text = notebook.description,
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.outline, // [DIUBAH]
|
color = MaterialTheme.colorScheme.outline,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
fontSize = 12.sp
|
fontSize = 12.sp
|
||||||
@ -498,7 +498,7 @@ fun NotebookCardItem(
|
|||||||
Icon(
|
Icon(
|
||||||
Icons.Default.MoreVert,
|
Icons.Default.MoreVert,
|
||||||
contentDescription = "Menu",
|
contentDescription = "Menu",
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant // [DIUBAH]
|
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -538,18 +538,18 @@ fun EmptyNotebookMessage() {
|
|||||||
Icons.Default.Description,
|
Icons.Default.Description,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(64.dp),
|
modifier = Modifier.size(64.dp),
|
||||||
tint = MaterialTheme.colorScheme.surfaceVariant // [DIUBAH]
|
tint = MaterialTheme.colorScheme.surfaceVariant
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
Text(
|
Text(
|
||||||
"Belum ada notebook",
|
"Belum ada notebook",
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant // [DIUBAH]
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
"Klik tombol di atas untuk membuat notebook baru",
|
"Klik tombol di atas untuk membuat notebook baru",
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.outline // [DIUBAH]
|
color = MaterialTheme.colorScheme.outline
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -636,7 +636,7 @@ fun CreateNotebookDialog(
|
|||||||
},
|
},
|
||||||
dismissButton = {
|
dismissButton = {
|
||||||
TextButton(onClick = onDismiss) {
|
TextButton(onClick = onDismiss) {
|
||||||
Text("Batal", color = MaterialTheme.colorScheme.onSurfaceVariant) // [DIUBAH]
|
Text("Batal", color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,14 +1,8 @@
|
|||||||
package com.example.notebook.api
|
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 {
|
object ApiConstants {
|
||||||
|
|
||||||
// GANTI INI DENGAN API KEY KAMU
|
// API KEY GEMINI (GOOGLE AI STUDIO)
|
||||||
const val GEMINI_API_KEY = "AIzaSyCVYFUMcKqCDKN5Z_vNwT2Z4VHgjJ5V7dI"
|
const val GEMINI_API_KEY = "AIzaSyCVYFUMcKqCDKN5Z_vNwT2Z4VHgjJ5V7dI"
|
||||||
|
|
||||||
// Endpoint Gemini API
|
// Endpoint Gemini API
|
||||||
@ -17,7 +11,7 @@ object ApiConstants {
|
|||||||
// Model yang digunakan (Flash = gratis & cepat)
|
// Model yang digunakan (Flash = gratis & cepat)
|
||||||
const val MODEL_NAME = "gemini-2.0-flash"
|
const val MODEL_NAME = "gemini-2.0-flash"
|
||||||
|
|
||||||
// System instruction untuk AI
|
// System instruction untuk AI nya
|
||||||
const val SYSTEM_INSTRUCTION = """
|
const val SYSTEM_INSTRUCTION = """
|
||||||
Anda adalah AI assistant yang membantu pengguna memahami dokumen mereka dengan lebih baik.
|
Anda adalah AI assistant yang membantu pengguna memahami dokumen mereka dengan lebih baik.
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import kotlinx.coroutines.delay
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Repository untuk handle Gemini API calls
|
* Repository untuk handle Gemini API calls
|
||||||
* ENHANCED: Better error handling & retry logic untuk 429 errors
|
|
||||||
*/
|
*/
|
||||||
class GeminiRepository {
|
class GeminiRepository {
|
||||||
|
|
||||||
@ -14,9 +13,7 @@ class GeminiRepository {
|
|||||||
private val maxRetries = 3
|
private val maxRetries = 3
|
||||||
private val initialDelayMs = 2000L // 2 seconds
|
private val initialDelayMs = 2000L // 2 seconds
|
||||||
|
|
||||||
/**
|
// Helper function untuk retry dengan exponential backoff
|
||||||
* Helper function untuk retry dengan exponential backoff
|
|
||||||
*/
|
|
||||||
private suspend fun <T> retryWithBackoff(
|
private suspend fun <T> retryWithBackoff(
|
||||||
maxAttempts: Int = maxRetries,
|
maxAttempts: Int = maxRetries,
|
||||||
initialDelay: Long = initialDelayMs,
|
initialDelay: Long = initialDelayMs,
|
||||||
@ -43,9 +40,7 @@ class GeminiRepository {
|
|||||||
return block() // Last attempt
|
return block() // Last attempt
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
//Generate summary dengan retry logic
|
||||||
* Generate summary dengan retry logic
|
|
||||||
*/
|
|
||||||
suspend fun generateSummary(text: String): Result<String> {
|
suspend fun generateSummary(text: String): Result<String> {
|
||||||
return try {
|
return try {
|
||||||
retryWithBackoff {
|
retryWithBackoff {
|
||||||
@ -95,9 +90,7 @@ class GeminiRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Generate summary dari PDF dengan retry logic
|
||||||
* Generate summary dari PDF dengan retry logic
|
|
||||||
*/
|
|
||||||
suspend fun generateSummaryFromPdfFile(
|
suspend fun generateSummaryFromPdfFile(
|
||||||
filePath: String,
|
filePath: String,
|
||||||
fileName: String
|
fileName: String
|
||||||
@ -180,9 +173,8 @@ class GeminiRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Chat dengan document - dengan retry logic
|
// Chat dengan document - dengan retry logic
|
||||||
*/
|
|
||||||
suspend fun chatWithDocument(
|
suspend fun chatWithDocument(
|
||||||
userMessage: String,
|
userMessage: String,
|
||||||
documentContext: String,
|
documentContext: String,
|
||||||
@ -286,9 +278,9 @@ class GeminiRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Chat dengan PDF file - dengan retry logic
|
// Chat dengan PDF file - dengan retry logic
|
||||||
*/
|
|
||||||
suspend fun chatWithPdfFile(
|
suspend fun chatWithPdfFile(
|
||||||
userMessage: String,
|
userMessage: String,
|
||||||
pdfFilePath: String,
|
pdfFilePath: String,
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import androidx.room.RoomDatabase
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Database utama aplikasi
|
* Database utama aplikasi
|
||||||
* Version 1 = versi pertama database kamu
|
* Version 1 = versi pertama database
|
||||||
*/
|
*/
|
||||||
@Database(
|
@Database(
|
||||||
entities = [
|
entities = [
|
||||||
|
|||||||
@ -10,8 +10,7 @@ import kotlinx.coroutines.flow.Flow
|
|||||||
@Dao
|
@Dao
|
||||||
interface NotebookDao {
|
interface NotebookDao {
|
||||||
|
|
||||||
// === NOTEBOOK OPERATIONS ===
|
// NOTEBOOK OPERATIONS
|
||||||
|
|
||||||
@Insert
|
@Insert
|
||||||
suspend fun insertNotebook(notebook: NotebookEntity): Long
|
suspend fun insertNotebook(notebook: NotebookEntity): Long
|
||||||
|
|
||||||
@ -27,7 +26,7 @@ interface NotebookDao {
|
|||||||
@Query("SELECT * FROM notebooks WHERE id = :notebookId")
|
@Query("SELECT * FROM notebooks WHERE id = :notebookId")
|
||||||
fun getNotebookById(notebookId: Int): Flow<NotebookEntity?>
|
fun getNotebookById(notebookId: Int): Flow<NotebookEntity?>
|
||||||
|
|
||||||
// === SOURCE OPERATIONS ===
|
// SOURCE OPERATIONS
|
||||||
|
|
||||||
@Insert
|
@Insert
|
||||||
suspend fun insertSource(source: SourceEntity)
|
suspend fun insertSource(source: SourceEntity)
|
||||||
@ -38,7 +37,7 @@ interface NotebookDao {
|
|||||||
@Delete
|
@Delete
|
||||||
suspend fun deleteSource(source: SourceEntity)
|
suspend fun deleteSource(source: SourceEntity)
|
||||||
|
|
||||||
// === CHAT OPERATIONS ===
|
// CHAT OPERATIONS
|
||||||
|
|
||||||
@Insert
|
@Insert
|
||||||
suspend fun insertChatMessage(message: ChatMessageEntity)
|
suspend fun insertChatMessage(message: ChatMessageEntity)
|
||||||
|
|||||||
@ -17,9 +17,7 @@ data class NotebookEntity(
|
|||||||
val sourceCount: Int = 0, // Jumlah sumber yang diupload
|
val sourceCount: Int = 0, // Jumlah sumber yang diupload
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
// Entity untuk menyimpan sumber/dokumen yang diupload
|
||||||
* Entity untuk menyimpan sumber/dokumen yang diupload
|
|
||||||
*/
|
|
||||||
@Entity(tableName = "sources")
|
@Entity(tableName = "sources")
|
||||||
data class SourceEntity(
|
data class SourceEntity(
|
||||||
@PrimaryKey(autoGenerate = true)
|
@PrimaryKey(autoGenerate = true)
|
||||||
@ -32,9 +30,8 @@ data class SourceEntity(
|
|||||||
val uploadedAt: Long // Timestamp upload
|
val uploadedAt: Long // Timestamp upload
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
|
||||||
* Entity untuk menyimpan chat history dengan AI
|
// Entity untuk menyimpan chat history dengan AI
|
||||||
*/
|
|
||||||
@Entity(tableName = "chat_messages")
|
@Entity(tableName = "chat_messages")
|
||||||
data class ChatMessageEntity(
|
data class ChatMessageEntity(
|
||||||
@PrimaryKey(autoGenerate = true)
|
@PrimaryKey(autoGenerate = true)
|
||||||
|
|||||||
@ -8,18 +8,14 @@ import kotlinx.coroutines.flow.Flow
|
|||||||
*/
|
*/
|
||||||
class NotebookRepository(private val dao: NotebookDao) {
|
class NotebookRepository(private val dao: NotebookDao) {
|
||||||
|
|
||||||
// === NOTEBOOK OPERATIONS ===
|
// NOTEBOOK OPERATIONS
|
||||||
|
|
||||||
/**
|
// Ambil semua notebooks (otomatis update kalau ada perubahan)
|
||||||
* Ambil semua notebooks (otomatis update kalau ada perubahan)
|
|
||||||
*/
|
|
||||||
fun getAllNotebooks(): Flow<List<NotebookEntity>> {
|
fun getAllNotebooks(): Flow<List<NotebookEntity>> {
|
||||||
return dao.getAllNotebooks()
|
return dao.getAllNotebooks()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Ambil notebook berdasarkan ID
|
||||||
* Ambil notebook berdasarkan ID
|
|
||||||
*/
|
|
||||||
fun getNotebookById(id: Int): Flow<NotebookEntity?> {
|
fun getNotebookById(id: Int): Flow<NotebookEntity?> {
|
||||||
return dao.getNotebookById(id)
|
return dao.getNotebookById(id)
|
||||||
}
|
}
|
||||||
@ -42,26 +38,20 @@ class NotebookRepository(private val dao: NotebookDao) {
|
|||||||
return dao.insertNotebook(notebook)
|
return dao.insertNotebook(notebook)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Update notebook yang sudah ada
|
||||||
* Update notebook yang sudah ada
|
|
||||||
*/
|
|
||||||
suspend fun updateNotebook(notebook: NotebookEntity) {
|
suspend fun updateNotebook(notebook: NotebookEntity) {
|
||||||
val updated = notebook.copy(updatedAt = System.currentTimeMillis())
|
val updated = notebook.copy(updatedAt = System.currentTimeMillis())
|
||||||
dao.updateNotebook(updated)
|
dao.updateNotebook(updated)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Hapus notebook
|
||||||
* Hapus notebook
|
|
||||||
*/
|
|
||||||
suspend fun deleteNotebook(notebook: NotebookEntity) {
|
suspend fun deleteNotebook(notebook: NotebookEntity) {
|
||||||
dao.deleteNotebook(notebook)
|
dao.deleteNotebook(notebook)
|
||||||
}
|
}
|
||||||
|
|
||||||
// === SOURCE OPERATIONS ===
|
// SOURCE OPERATIONS
|
||||||
|
|
||||||
/**
|
// Tambah source ke notebook
|
||||||
* Tambah source ke notebook
|
|
||||||
*/
|
|
||||||
suspend fun addSource(
|
suspend fun addSource(
|
||||||
notebookId: Int,
|
notebookId: Int,
|
||||||
fileName: String,
|
fileName: String,
|
||||||
@ -82,25 +72,19 @@ class NotebookRepository(private val dao: NotebookDao) {
|
|||||||
// Note: Ini simplified, di production pakai query COUNT
|
// Note: Ini simplified, di production pakai query COUNT
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Ambil semua sources dalam notebook
|
||||||
* Ambil semua sources dalam notebook
|
|
||||||
*/
|
|
||||||
fun getSourcesByNotebook(notebookId: Int): Flow<List<SourceEntity>> {
|
fun getSourcesByNotebook(notebookId: Int): Flow<List<SourceEntity>> {
|
||||||
return dao.getSourcesByNotebook(notebookId)
|
return dao.getSourcesByNotebook(notebookId)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Hapus source
|
||||||
* Hapus source
|
|
||||||
*/
|
|
||||||
suspend fun deleteSource(source: SourceEntity) {
|
suspend fun deleteSource(source: SourceEntity) {
|
||||||
dao.deleteSource(source)
|
dao.deleteSource(source)
|
||||||
}
|
}
|
||||||
|
|
||||||
// === CHAT OPERATIONS ===
|
// CHAT OPERATIONS
|
||||||
|
|
||||||
/**
|
// Kirim pesan chat (user atau AI)
|
||||||
* Kirim pesan chat (user atau AI)
|
|
||||||
*/
|
|
||||||
suspend fun sendMessage(
|
suspend fun sendMessage(
|
||||||
notebookId: Int,
|
notebookId: Int,
|
||||||
message: String,
|
message: String,
|
||||||
@ -115,25 +99,19 @@ class NotebookRepository(private val dao: NotebookDao) {
|
|||||||
dao.insertChatMessage(chatMessage)
|
dao.insertChatMessage(chatMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Ambil history chat
|
||||||
* Ambil history chat
|
|
||||||
*/
|
|
||||||
fun getChatHistory(notebookId: Int): Flow<List<ChatMessageEntity>> {
|
fun getChatHistory(notebookId: Int): Flow<List<ChatMessageEntity>> {
|
||||||
return dao.getChatHistory(notebookId)
|
return dao.getChatHistory(notebookId)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Hapus semua chat dalam notebook
|
||||||
* Hapus semua chat dalam notebook
|
|
||||||
*/
|
|
||||||
suspend fun clearChatHistory(notebookId: Int) {
|
suspend fun clearChatHistory(notebookId: Int) {
|
||||||
dao.clearChatHistory(notebookId)
|
dao.clearChatHistory(notebookId)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Hapus pesan AI terakhir (untuk replace dengan hasil sebenarnya)
|
||||||
* Hapus pesan AI terakhir (untuk replace dengan hasil sebenarnya)
|
|
||||||
*/
|
|
||||||
suspend fun clearLastAIMessage(notebookId: Int) {
|
suspend fun clearLastAIMessage(notebookId: Int) {
|
||||||
// Get last AI message
|
// Get last AI message kaya di GPT bisa di Edit prompt nya
|
||||||
// Note: Ini simplified, di production sebaiknya pakai query langsung
|
// Note: Ini simplified, di production sebaiknya pakai query langsung
|
||||||
// Untuk sekarang kita skip dulu, cukup pakai pesan "thinking" yang nanti di-override
|
// Untuk sekarang kita skip dulu, cukup pakai pesan "thinking" yang nanti di-override
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,7 +19,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Composable untuk render markdown text
|
* Composable untuk render markdown text (Parser)
|
||||||
* Support: **bold**, *italic*, `code`, # heading, numbered list, bullet list, TABLES
|
* Support: **bold**, *italic*, `code`, # heading, numbered list, bullet list, TABLES
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
@ -100,9 +100,7 @@ fun MarkdownText(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Render tabel dengan styling yang bagus
|
||||||
* Render tabel dengan styling yang bagus
|
|
||||||
*/
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MarkdownTable(
|
fun MarkdownTable(
|
||||||
headers: List<String>,
|
headers: List<String>,
|
||||||
@ -162,9 +160,7 @@ fun MarkdownTable(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Build styled text dengan support inline markdown
|
||||||
* Build styled text dengan support inline markdown
|
|
||||||
*/
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun buildStyledText(text: String) = buildAnnotatedString {
|
private fun buildStyledText(text: String) = buildAnnotatedString {
|
||||||
var currentIndex = 0
|
var currentIndex = 0
|
||||||
@ -223,10 +219,7 @@ private fun buildStyledText(text: String) = buildAnnotatedString {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Parse markdown string menjadi list of elements
|
||||||
* Parse markdown string menjadi list of elements
|
|
||||||
* UPGRADED: Support untuk tabel
|
|
||||||
*/
|
|
||||||
private fun parseMarkdown(markdown: String): List<MarkdownElement> {
|
private fun parseMarkdown(markdown: String): List<MarkdownElement> {
|
||||||
val lines = markdown.lines()
|
val lines = markdown.lines()
|
||||||
val elements = mutableListOf<MarkdownElement>()
|
val elements = mutableListOf<MarkdownElement>()
|
||||||
@ -315,10 +308,7 @@ private fun parseMarkdown(markdown: String): List<MarkdownElement> {
|
|||||||
return elements
|
return elements
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Markdown Elements
|
||||||
* Sealed class untuk markdown elements
|
|
||||||
* UPGRADED: Tambah Table
|
|
||||||
*/
|
|
||||||
private sealed class MarkdownElement {
|
private sealed class MarkdownElement {
|
||||||
data class Paragraph(val text: String) : MarkdownElement()
|
data class Paragraph(val text: String) : MarkdownElement()
|
||||||
data class Heading(val level: Int, val text: String) : MarkdownElement()
|
data class Heading(val level: Int, val text: String) : MarkdownElement()
|
||||||
|
|||||||
@ -61,13 +61,13 @@ fun openFile(context: Context, source: SourceEntity) {
|
|||||||
|
|
||||||
val mimeType = when (source.fileType) {
|
val mimeType = when (source.fileType) {
|
||||||
"PDF" -> "application/pdf"
|
"PDF" -> "application/pdf"
|
||||||
"Image" -> "image/*"
|
"Image" -> "image/*" // Belum Support
|
||||||
"Text" -> "text/plain"
|
"Text" -> "text/plain"
|
||||||
"Markdown" -> "text/markdown"
|
"Markdown" -> "text/markdown"
|
||||||
"Word" -> "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
"Word" -> "application/vnd.openxmlformats-officedocument.wordprocessingml.document" // Belum Support
|
||||||
"PowerPoint" -> "application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
"PowerPoint" -> "application/vnd.openxmlformats-officedocument.presentationml.presentation" // Belum Support
|
||||||
"Audio" -> "audio/*"
|
"Audio" -> "audio/*" // Belum Support
|
||||||
"Video" -> "video/*"
|
"Video" -> "video/*" // Belum Support
|
||||||
else -> "*/*"
|
else -> "*/*"
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -589,6 +589,7 @@ fun TypingIndicator() {
|
|||||||
@Composable
|
@Composable
|
||||||
fun ChatBubble(message: ChatMessageEntity) {
|
fun ChatBubble(message: ChatMessageEntity) {
|
||||||
val dateFormat = SimpleDateFormat("HH:mm", Locale.getDefault())
|
val dateFormat = SimpleDateFormat("HH:mm", Locale.getDefault())
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
@ -625,7 +626,22 @@ fun ChatBubble(message: ChatMessageEntity) {
|
|||||||
bottomStart = if (message.isUserMessage) 20.dp else 4.dp,
|
bottomStart = if (message.isUserMessage) 20.dp else 4.dp,
|
||||||
bottomEnd = if (message.isUserMessage) 4.dp else 20.dp
|
bottomEnd = if (message.isUserMessage) 4.dp else 20.dp
|
||||||
),
|
),
|
||||||
shadowElevation = if (!message.isUserMessage) 1.dp else 0.dp
|
shadowElevation = if (!message.isUserMessage) 1.dp else 0.dp,
|
||||||
|
modifier = if (!message.isUserMessage) {
|
||||||
|
Modifier.clickable {
|
||||||
|
// Copy text ke clipboard
|
||||||
|
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager
|
||||||
|
val clip = android.content.ClipData.newPlainText("AI Response", message.message)
|
||||||
|
clipboard.setPrimaryClip(clip)
|
||||||
|
|
||||||
|
// Show toast feedback
|
||||||
|
android.widget.Toast.makeText(
|
||||||
|
context,
|
||||||
|
"✅ Teks disalin!",
|
||||||
|
android.widget.Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
} else Modifier
|
||||||
) {
|
) {
|
||||||
if (message.isUserMessage) {
|
if (message.isUserMessage) {
|
||||||
Text(
|
Text(
|
||||||
@ -637,17 +653,33 @@ fun ChatBubble(message: ChatMessageEntity) {
|
|||||||
} else {
|
} else {
|
||||||
MarkdownText(
|
MarkdownText(
|
||||||
markdown = message.message,
|
markdown = message.message,
|
||||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp)
|
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
|
color = TextPrimary
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(
|
Row(
|
||||||
text = dateFormat.format(Date(message.timestamp)),
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
fontSize = 11.sp,
|
|
||||||
color = TextSecondary,
|
|
||||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)
|
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||||
)
|
) {
|
||||||
|
Text(
|
||||||
|
text = dateFormat.format(Date(message.timestamp)),
|
||||||
|
fontSize = 11.sp,
|
||||||
|
color = TextSecondary
|
||||||
|
)
|
||||||
|
|
||||||
|
// Copy indicator untuk AI message
|
||||||
|
if (!message.isUserMessage) {
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Icon(
|
||||||
|
Icons.Default.ContentCopy,
|
||||||
|
contentDescription = "Tap to copy",
|
||||||
|
tint = TextSecondary.copy(alpha = 0.6f),
|
||||||
|
modifier = Modifier.size(12.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message.isUserMessage) {
|
if (message.isUserMessage) {
|
||||||
|
|||||||
@ -42,9 +42,7 @@ object FileHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Ambil nama file
|
||||||
* Ambil nama file dari URI
|
|
||||||
*/
|
|
||||||
fun getFileName(context: Context, uri: Uri): String? {
|
fun getFileName(context: Context, uri: Uri): String? {
|
||||||
var fileName: String? = null
|
var fileName: String? = null
|
||||||
|
|
||||||
@ -60,9 +58,7 @@ object FileHelper {
|
|||||||
return fileName
|
return fileName
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Deteksi tipe file dari extension
|
||||||
* Deteksi tipe file dari extension
|
|
||||||
*/
|
|
||||||
fun getFileType(fileName: String): String {
|
fun getFileType(fileName: String): String {
|
||||||
val type = when (fileName.substringAfterLast('.').lowercase()) {
|
val type = when (fileName.substringAfterLast('.').lowercase()) {
|
||||||
"pdf" -> "PDF"
|
"pdf" -> "PDF"
|
||||||
@ -79,9 +75,7 @@ object FileHelper {
|
|||||||
return type
|
return type
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Baca text dari file (support Text, Markdown, dan PDF)
|
||||||
* Baca text dari file (support Text, Markdown, dan PDF)
|
|
||||||
*/
|
|
||||||
fun readTextFromFile(filePath: String): String? {
|
fun readTextFromFile(filePath: String): String? {
|
||||||
return try {
|
return try {
|
||||||
val file = File(filePath)
|
val file = File(filePath)
|
||||||
@ -107,9 +101,7 @@ object FileHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Format ukuran file
|
||||||
* Format ukuran file
|
|
||||||
*/
|
|
||||||
fun formatFileSize(size: Long): String {
|
fun formatFileSize(size: Long): String {
|
||||||
if (size < 1024) return "$size B"
|
if (size < 1024) return "$size B"
|
||||||
val kb = size / 1024.0
|
val kb = size / 1024.0
|
||||||
@ -120,9 +112,7 @@ object FileHelper {
|
|||||||
return "%.2f GB".format(gb)
|
return "%.2f GB".format(gb)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Hapus file
|
||||||
* Hapus file
|
|
||||||
*/
|
|
||||||
fun deleteFile(filePath: String): Boolean {
|
fun deleteFile(filePath: String): Boolean {
|
||||||
return try {
|
return try {
|
||||||
File(filePath).delete()
|
File(filePath).delete()
|
||||||
|
|||||||
@ -12,9 +12,7 @@ import java.io.File
|
|||||||
*/
|
*/
|
||||||
object PdfHelper {
|
object PdfHelper {
|
||||||
|
|
||||||
/**
|
// Initialize PDFBox (panggil sekali saat app start)
|
||||||
* Initialize PDFBox (panggil sekali saat app start)
|
|
||||||
*/
|
|
||||||
fun initialize(context: Context) {
|
fun initialize(context: Context) {
|
||||||
PDFBoxResourceLoader.init(context)
|
PDFBoxResourceLoader.init(context)
|
||||||
}
|
}
|
||||||
@ -42,9 +40,7 @@ object PdfHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Extract text dari PDF (sebelum disimpan)
|
||||||
* Extract text dari PDF URI (sebelum disimpan)
|
|
||||||
*/
|
|
||||||
fun extractTextFromPdfUri(context: Context, uri: Uri): String? {
|
fun extractTextFromPdfUri(context: Context, uri: Uri): String? {
|
||||||
return try {
|
return try {
|
||||||
val inputStream = context.contentResolver.openInputStream(uri)
|
val inputStream = context.contentResolver.openInputStream(uri)
|
||||||
@ -64,9 +60,7 @@ object PdfHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Get info PDF (jumlah halaman, dll)
|
||||||
* Get info PDF (jumlah halaman, dll)
|
|
||||||
*/
|
|
||||||
fun getPdfInfo(filePath: String): PdfInfo? {
|
fun getPdfInfo(filePath: String): PdfInfo? {
|
||||||
return try {
|
return try {
|
||||||
val file = File(filePath)
|
val file = File(filePath)
|
||||||
|
|||||||
@ -289,7 +289,7 @@ class NotebookViewModel(application: Application) : AndroidViewModel(application
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// === GEMINI FUNCTIONS ===
|
// GEMINI FUNCTIONS
|
||||||
|
|
||||||
private fun buildDocumentContext(): String {
|
private fun buildDocumentContext(): String {
|
||||||
val context = StringBuilder()
|
val context = StringBuilder()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user