Refinement
This commit is contained in:
parent
a51c24030f
commit
b1d99a37bb
@ -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)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -7,7 +7,7 @@ import androidx.room.RoomDatabase
|
||||
|
||||
/**
|
||||
* Database utama aplikasi
|
||||
* Version 1 = versi pertama database kamu
|
||||
* Version 1 = versi pertama database
|
||||
*/
|
||||
@Database(
|
||||
entities = [
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||
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) {
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -289,7 +289,7 @@ class NotebookViewModel(application: Application) : AndroidViewModel(application
|
||||
}
|
||||
}
|
||||
|
||||
// === GEMINI FUNCTIONS ===
|
||||
// GEMINI FUNCTIONS
|
||||
|
||||
private fun buildDocumentContext(): String {
|
||||
val context = StringBuilder()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user