Refinement

This commit is contained in:
202310715297 RAIHAN ARIQ MUZAKKI 2025-11-18 16:06:42 +07:00
parent a51c24030f
commit b1d99a37bb
12 changed files with 110 additions and 144 deletions

View File

@ -49,7 +49,7 @@ class MainActivity : ComponentActivity() {
NotebookTheme(darkTheme = isDarkMode) {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background // [DIUBAH] Menggunakan warna tema
color = MaterialTheme.colorScheme.background
) {
NotebookBottomSheetApp(
viewModel = viewModel,
@ -99,7 +99,7 @@ fun NotebookBottomSheetApp(
Box(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.background) // [DIUBAH] Menggunakan warna tema
.background(MaterialTheme.colorScheme.background)
) {
Column(
modifier = Modifier
@ -109,7 +109,7 @@ fun NotebookBottomSheetApp(
shape = 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(
isDarkMode = isDarkMode,
@ -134,12 +134,12 @@ fun NotebookBottomSheetApp(
"Notebook Saya",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface // [DIUBAH]
color = MaterialTheme.colorScheme.onSurface
)
Text(
"${notebooks.size} notebook tersimpan",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant // [DIUBAH]
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
@ -208,12 +208,12 @@ fun NotebookHeader(
"NoteBook",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface // [DIUBAH]
color = MaterialTheme.colorScheme.onSurface
)
Text(
"Selamat datang kembali!",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant, // [DIUBAH]
color = MaterialTheme.colorScheme.onSurfaceVariant,
fontSize = 11.sp
)
}
@ -224,7 +224,7 @@ fun NotebookHeader(
Icon(
imageVector = if (isDarkMode) Icons.Default.LightMode else Icons.Default.DarkMode,
contentDescription = "Toggle Theme",
tint = MaterialTheme.colorScheme.onSurfaceVariant // [DIUBAH]
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
}
@ -269,10 +269,10 @@ fun AccountScreen(onDismiss: () -> Unit) {
modifier = Modifier
.size(80.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.primary), // [DIUBAH]
.background(MaterialTheme.colorScheme.primary),
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))
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)
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()) {
Text("Kelola Akun Google Anda")
}
@ -434,7 +434,7 @@ fun NotebookCardItem(
.clickable(onClick = onClick),
shape = RoundedCornerShape(12.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceContainerLowest // [DIUBAH]
containerColor = MaterialTheme.colorScheme.surfaceContainerLowest
),
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp)
) {
@ -468,13 +468,13 @@ fun NotebookCardItem(
fontWeight = FontWeight.Bold,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = MaterialTheme.colorScheme.onSurface // [DIUBAH]
color = MaterialTheme.colorScheme.onSurface
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = dateFormat.format(Date(notebook.updatedAt)),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant, // [DIUBAH]
color = MaterialTheme.colorScheme.onSurfaceVariant,
fontSize = 12.sp
)
if (notebook.description.isNotBlank()) {
@ -482,7 +482,7 @@ fun NotebookCardItem(
Text(
text = notebook.description,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.outline, // [DIUBAH]
color = MaterialTheme.colorScheme.outline,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
fontSize = 12.sp
@ -498,7 +498,7 @@ fun NotebookCardItem(
Icon(
Icons.Default.MoreVert,
contentDescription = "Menu",
tint = MaterialTheme.colorScheme.onSurfaceVariant // [DIUBAH]
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
}
@ -538,18 +538,18 @@ fun EmptyNotebookMessage() {
Icons.Default.Description,
contentDescription = null,
modifier = Modifier.size(64.dp),
tint = MaterialTheme.colorScheme.surfaceVariant // [DIUBAH]
tint = MaterialTheme.colorScheme.surfaceVariant
)
Spacer(modifier = Modifier.height(16.dp))
Text(
"Belum ada notebook",
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant // [DIUBAH]
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
"Klik tombol di atas untuk membuat notebook baru",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.outline // [DIUBAH]
color = MaterialTheme.colorScheme.outline
)
}
}
@ -636,7 +636,7 @@ fun CreateNotebookDialog(
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text("Batal", color = MaterialTheme.colorScheme.onSurfaceVariant) // [DIUBAH]
Text("Batal", color = MaterialTheme.colorScheme.onSurfaceVariant)
}
}
)

View File

@ -1,14 +1,8 @@
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
// API KEY GEMINI (GOOGLE AI STUDIO)
const val GEMINI_API_KEY = "AIzaSyCVYFUMcKqCDKN5Z_vNwT2Z4VHgjJ5V7dI"
// Endpoint Gemini API
@ -17,7 +11,7 @@ object ApiConstants {
// Model yang digunakan (Flash = gratis & cepat)
const val MODEL_NAME = "gemini-2.0-flash"
// System instruction untuk AI
// System instruction untuk AI nya
const val SYSTEM_INSTRUCTION = """
Anda adalah AI assistant yang membantu pengguna memahami dokumen mereka dengan lebih baik.

View File

@ -4,7 +4,6 @@ import kotlinx.coroutines.delay
/**
* Repository untuk handle Gemini API calls
* ENHANCED: Better error handling & retry logic untuk 429 errors
*/
class GeminiRepository {
@ -14,9 +13,7 @@ class GeminiRepository {
private val maxRetries = 3
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(
maxAttempts: Int = maxRetries,
initialDelay: Long = initialDelayMs,
@ -43,9 +40,7 @@ class GeminiRepository {
return block() // Last attempt
}
/**
* Generate summary dengan retry logic
*/
//Generate summary dengan retry logic
suspend fun generateSummary(text: String): Result<String> {
return try {
retryWithBackoff {
@ -95,9 +90,7 @@ class GeminiRepository {
}
}
/**
* Generate summary dari PDF dengan retry logic
*/
// Generate summary dari PDF dengan retry logic
suspend fun generateSummaryFromPdfFile(
filePath: String,
fileName: String
@ -180,9 +173,8 @@ class GeminiRepository {
}
}
/**
* Chat dengan document - dengan retry logic
*/
// Chat dengan document - dengan retry logic
suspend fun chatWithDocument(
userMessage: 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(
userMessage: String,
pdfFilePath: String,

View File

@ -7,7 +7,7 @@ import androidx.room.RoomDatabase
/**
* Database utama aplikasi
* Version 1 = versi pertama database kamu
* Version 1 = versi pertama database
*/
@Database(
entities = [

View File

@ -10,8 +10,7 @@ import kotlinx.coroutines.flow.Flow
@Dao
interface NotebookDao {
// === NOTEBOOK OPERATIONS ===
// NOTEBOOK OPERATIONS
@Insert
suspend fun insertNotebook(notebook: NotebookEntity): Long
@ -27,7 +26,7 @@ interface NotebookDao {
@Query("SELECT * FROM notebooks WHERE id = :notebookId")
fun getNotebookById(notebookId: Int): Flow<NotebookEntity?>
// === SOURCE OPERATIONS ===
// SOURCE OPERATIONS
@Insert
suspend fun insertSource(source: SourceEntity)
@ -38,7 +37,7 @@ interface NotebookDao {
@Delete
suspend fun deleteSource(source: SourceEntity)
// === CHAT OPERATIONS ===
// CHAT OPERATIONS
@Insert
suspend fun insertChatMessage(message: ChatMessageEntity)

View File

@ -17,9 +17,7 @@ data class NotebookEntity(
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")
data class SourceEntity(
@PrimaryKey(autoGenerate = true)
@ -32,9 +30,8 @@ data class SourceEntity(
val uploadedAt: Long // Timestamp upload
)
/**
* Entity untuk menyimpan chat history dengan AI
*/
// Entity untuk menyimpan chat history dengan AI
@Entity(tableName = "chat_messages")
data class ChatMessageEntity(
@PrimaryKey(autoGenerate = true)

View File

@ -8,18 +8,14 @@ import kotlinx.coroutines.flow.Flow
*/
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>> {
return dao.getAllNotebooks()
}
/**
* Ambil notebook berdasarkan ID
*/
// Ambil notebook berdasarkan ID
fun getNotebookById(id: Int): Flow<NotebookEntity?> {
return dao.getNotebookById(id)
}
@ -42,26 +38,20 @@ class NotebookRepository(private val dao: NotebookDao) {
return dao.insertNotebook(notebook)
}
/**
* Update notebook yang sudah ada
*/
// Update notebook yang sudah ada
suspend fun updateNotebook(notebook: NotebookEntity) {
val updated = notebook.copy(updatedAt = System.currentTimeMillis())
dao.updateNotebook(updated)
}
/**
* Hapus notebook
*/
// Hapus notebook
suspend fun deleteNotebook(notebook: NotebookEntity) {
dao.deleteNotebook(notebook)
}
// === SOURCE OPERATIONS ===
// SOURCE OPERATIONS
/**
* Tambah source ke notebook
*/
// Tambah source ke notebook
suspend fun addSource(
notebookId: Int,
fileName: String,
@ -82,25 +72,19 @@ class NotebookRepository(private val dao: NotebookDao) {
// 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>> {
return dao.getSourcesByNotebook(notebookId)
}
/**
* Hapus source
*/
// Hapus source
suspend fun deleteSource(source: SourceEntity) {
dao.deleteSource(source)
}
// === CHAT OPERATIONS ===
// CHAT OPERATIONS
/**
* Kirim pesan chat (user atau AI)
*/
// Kirim pesan chat (user atau AI)
suspend fun sendMessage(
notebookId: Int,
message: String,
@ -115,25 +99,19 @@ class NotebookRepository(private val dao: NotebookDao) {
dao.insertChatMessage(chatMessage)
}
/**
* Ambil history chat
*/
// Ambil history chat
fun getChatHistory(notebookId: Int): Flow<List<ChatMessageEntity>> {
return dao.getChatHistory(notebookId)
}
/**
* Hapus semua chat dalam notebook
*/
// Hapus semua chat dalam notebook
suspend fun clearChatHistory(notebookId: Int) {
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) {
// 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
// Untuk sekarang kita skip dulu, cukup pakai pesan "thinking" yang nanti di-override
}

View File

@ -19,7 +19,7 @@ import androidx.compose.ui.unit.dp
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
*/
@Composable
@ -100,9 +100,7 @@ fun MarkdownText(
}
}
/**
* Render tabel dengan styling yang bagus
*/
// Render tabel dengan styling yang bagus
@Composable
fun MarkdownTable(
headers: List<String>,
@ -162,9 +160,7 @@ fun MarkdownTable(
}
}
/**
* Build styled text dengan support inline markdown
*/
// Build styled text dengan support inline markdown
@Composable
private fun buildStyledText(text: String) = buildAnnotatedString {
var currentIndex = 0
@ -223,10 +219,7 @@ private fun buildStyledText(text: String) = buildAnnotatedString {
}
}
/**
* Parse markdown string menjadi list of elements
* UPGRADED: Support untuk tabel
*/
// Parse markdown string menjadi list of elements
private fun parseMarkdown(markdown: String): List<MarkdownElement> {
val lines = markdown.lines()
val elements = mutableListOf<MarkdownElement>()
@ -315,10 +308,7 @@ private fun parseMarkdown(markdown: String): List<MarkdownElement> {
return elements
}
/**
* Sealed class untuk markdown elements
* UPGRADED: Tambah Table
*/
// Markdown Elements
private sealed class MarkdownElement {
data class Paragraph(val text: String) : MarkdownElement()
data class Heading(val level: Int, val text: String) : MarkdownElement()

View File

@ -61,13 +61,13 @@ fun openFile(context: Context, source: SourceEntity) {
val mimeType = when (source.fileType) {
"PDF" -> "application/pdf"
"Image" -> "image/*"
"Image" -> "image/*" // Belum Support
"Text" -> "text/plain"
"Markdown" -> "text/markdown"
"Word" -> "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
"PowerPoint" -> "application/vnd.openxmlformats-officedocument.presentationml.presentation"
"Audio" -> "audio/*"
"Video" -> "video/*"
"Word" -> "application/vnd.openxmlformats-officedocument.wordprocessingml.document" // Belum Support
"PowerPoint" -> "application/vnd.openxmlformats-officedocument.presentationml.presentation" // Belum Support
"Audio" -> "audio/*" // Belum Support
"Video" -> "video/*" // Belum Support
else -> "*/*"
}
@ -589,6 +589,7 @@ fun TypingIndicator() {
@Composable
fun ChatBubble(message: ChatMessageEntity) {
val dateFormat = SimpleDateFormat("HH:mm", Locale.getDefault())
val context = LocalContext.current
Row(
modifier = Modifier.fillMaxWidth(),
@ -625,7 +626,22 @@ fun ChatBubble(message: ChatMessageEntity) {
bottomStart = if (message.isUserMessage) 20.dp else 4.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) {
Text(
@ -637,17 +653,33 @@ fun ChatBubble(message: ChatMessageEntity) {
} else {
MarkdownText(
markdown = message.message,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp)
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
color = TextPrimary
)
}
}
Text(
text = dateFormat.format(Date(message.timestamp)),
fontSize = 11.sp,
color = TextSecondary,
Row(
verticalAlignment = Alignment.CenterVertically,
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) {

View File

@ -42,9 +42,7 @@ object FileHelper {
}
}
/**
* Ambil nama file dari URI
*/
// Ambil nama file
fun getFileName(context: Context, uri: Uri): String? {
var fileName: String? = null
@ -60,9 +58,7 @@ object FileHelper {
return fileName
}
/**
* Deteksi tipe file dari extension
*/
// Deteksi tipe file dari extension
fun getFileType(fileName: String): String {
val type = when (fileName.substringAfterLast('.').lowercase()) {
"pdf" -> "PDF"
@ -79,9 +75,7 @@ object FileHelper {
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? {
return try {
val file = File(filePath)
@ -107,9 +101,7 @@ object FileHelper {
}
}
/**
* Format ukuran file
*/
// Format ukuran file
fun formatFileSize(size: Long): String {
if (size < 1024) return "$size B"
val kb = size / 1024.0
@ -120,9 +112,7 @@ object FileHelper {
return "%.2f GB".format(gb)
}
/**
* Hapus file
*/
// Hapus file
fun deleteFile(filePath: String): Boolean {
return try {
File(filePath).delete()

View File

@ -12,9 +12,7 @@ import java.io.File
*/
object PdfHelper {
/**
* Initialize PDFBox (panggil sekali saat app start)
*/
// Initialize PDFBox (panggil sekali saat app start)
fun initialize(context: Context) {
PDFBoxResourceLoader.init(context)
}
@ -42,9 +40,7 @@ object PdfHelper {
}
}
/**
* Extract text dari PDF URI (sebelum disimpan)
*/
// Extract text dari PDF (sebelum disimpan)
fun extractTextFromPdfUri(context: Context, uri: Uri): String? {
return try {
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? {
return try {
val file = File(filePath)

View File

@ -289,7 +289,7 @@ class NotebookViewModel(application: Application) : AndroidViewModel(application
}
}
// === GEMINI FUNCTIONS ===
// GEMINI FUNCTIONS
private fun buildDocumentContext(): String {
val context = StringBuilder()