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) { 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)
} }
} }
) )

View File

@ -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.

View File

@ -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,

View File

@ -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 = [

View File

@ -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)

View File

@ -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)

View File

@ -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
} }

View File

@ -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()

View File

@ -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) {

View File

@ -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()

View File

@ -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)

View File

@ -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()