diff --git a/app/src/main/java/com/example/notebook/ui/screen/NotebookDetailScreen.kt b/app/src/main/java/com/example/notebook/ui/screen/NotebookDetailScreen.kt index 97b86f7..662f4da 100644 --- a/app/src/main/java/com/example/notebook/ui/screen/NotebookDetailScreen.kt +++ b/app/src/main/java/com/example/notebook/ui/screen/NotebookDetailScreen.kt @@ -5,12 +5,13 @@ import android.content.Intent import android.net.Uri import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.animation.core.* import androidx.compose.foundation.background -import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons @@ -30,10 +31,10 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.core.content.FileProvider import com.example.notebook.data.ChatMessageEntity -import com.example.notebook.data.NotebookEntity import com.example.notebook.data.SourceEntity import com.example.notebook.ui.components.MarkdownText import com.example.notebook.viewmodel.NotebookViewModel +import kotlinx.coroutines.launch import java.io.File import java.text.SimpleDateFormat import java.util.* @@ -58,7 +59,6 @@ fun openFile(context: Context, source: SourceEntity) { return } - // Tentukan MIME type val mimeType = when (source.fileType) { "PDF" -> "application/pdf" "Image" -> "image/*" @@ -71,7 +71,6 @@ fun openFile(context: Context, source: SourceEntity) { else -> "*/*" } - // Gunakan FileProvider untuk file di internal storage val uri = FileProvider.getUriForFile( context, "${context.packageName}.fileprovider", @@ -84,11 +83,9 @@ fun openFile(context: Context, source: SourceEntity) { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) } - // Cek apakah ada aplikasi yang bisa handle if (intent.resolveActivity(context.packageManager) != null) { context.startActivity(intent) } else { - // Fallback: buka dengan chooser val chooser = Intent.createChooser(intent, "Buka dengan") context.startActivity(chooser) } @@ -160,7 +157,7 @@ fun NotebookDetailScreen( } } - // Delete confirmation dialog dengan desain modern + // Delete confirmation dialog if (showDeleteDialog && sourceToDelete != null) { AlertDialog( onDismissRequest = { showDeleteDialog = false }, @@ -204,7 +201,6 @@ fun NotebookDetailScreen( ) } - // File picker launcher val filePickerLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.GetContent() ) { uri: Uri? -> @@ -213,13 +209,11 @@ fun NotebookDetailScreen( } } - // Load notebook data LaunchedEffect(notebookId) { println("🚀 NotebookDetailScreen: LaunchedEffect triggered untuk ID: $notebookId") viewModel.selectNotebook(notebookId) } - // Debug log untuk state changes LaunchedEffect(notebook) { println("📝 Notebook state updated: ${notebook?.title ?: "NULL"}") } @@ -309,51 +303,6 @@ fun NotebookDetailScreen( Icon(Icons.Default.Summarize, null, tint = PrimaryPurple) } ) - // Debug: Test PDF extraction - Divider(modifier = Modifier.padding(vertical = 4.dp)) - DropdownMenuItem( - text = { - Text( - "🔍 Test PDF Extract", - fontWeight = FontWeight.Medium, - color = TextPrimary - ) - }, - onClick = { - showUploadMenu = false - val pdfSource = sources.firstOrNull { it.fileType == "PDF" } - if (pdfSource != null) { - println("🔍 Testing PDF: ${pdfSource.fileName}") - val result = com.example.notebook.utils.PdfHelper.extractTextFromPdf(pdfSource.filePath) - if (result != null) { - println("📊 Full text length: ${result.length} karakter") - println("📝 First 500 chars: ${result.take(500)}") - println("📝 Last 500 chars: ${result.takeLast(500)}") - - android.widget.Toast.makeText( - context, - "✅ Extracted ${result.length} chars\nPreview: ${result.take(100)}...", - android.widget.Toast.LENGTH_LONG - ).show() - } else { - android.widget.Toast.makeText( - context, - "❌ PDF extraction returned null!", - android.widget.Toast.LENGTH_LONG - ).show() - } - } else { - android.widget.Toast.makeText( - context, - "⚠️ Tidak ada PDF yang diupload", - android.widget.Toast.LENGTH_SHORT - ).show() - } - }, - leadingIcon = { - Icon(Icons.Default.BugReport, null, tint = PrimaryPurple) - } - ) } } } @@ -364,7 +313,7 @@ fun NotebookDetailScreen( ) }, bottomBar = { - if (selectedTab == 0) { // Chat tab + if (selectedTab == 0) { Surface( color = CardBackground, shadowElevation = 8.dp @@ -457,7 +406,8 @@ fun NotebookDetailScreen( 0 -> ChatTab( messages = chatMessages, sources = sources, - onUploadClick = { filePickerLauncher.launch("*/*") } + onUploadClick = { filePickerLauncher.launch("*/*") }, + isLoading = isLoading ) 1 -> SourcesTab( sources = sources, @@ -479,9 +429,22 @@ fun NotebookDetailScreen( fun ChatTab( messages: List, sources: List, - onUploadClick: () -> Unit + onUploadClick: () -> Unit, + isLoading: Boolean = false ) { - if (messages.isEmpty()) { + val listState = rememberLazyListState() + val coroutineScope = rememberCoroutineScope() + + // Auto scroll ke chat terbaru + LaunchedEffect(messages.size) { + if (messages.isNotEmpty()) { + coroutineScope.launch { + listState.animateScrollToItem(messages.size) + } + } + } + + if (messages.isEmpty() && !isLoading) { // Empty state dengan desain modern Column( modifier = Modifier @@ -538,6 +501,7 @@ fun ChatTab( } } else { LazyColumn( + state = listState, modifier = Modifier .fillMaxSize() .background(BackgroundWhite), @@ -547,6 +511,77 @@ fun ChatTab( items(messages) { message -> ChatBubble(message = message) } + + // Typing indicator saat AI sedang memproses + if (isLoading) { + item { + TypingIndicator() + } + } + } + } +} + +@Composable +fun TypingIndicator() { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Start + ) { + // AI Avatar + Box( + modifier = Modifier + .size(36.dp) + .clip(CircleShape) + .background(LightPurple), + contentAlignment = Alignment.Center + ) { + Icon( + Icons.Default.AutoAwesome, + contentDescription = null, + tint = PrimaryPurple, + modifier = Modifier.size(20.dp) + ) + } + Spacer(modifier = Modifier.width(8.dp)) + + // Typing animation card + Surface( + color = CardBackground, + shape = RoundedCornerShape( + topStart = 20.dp, + topEnd = 20.dp, + bottomStart = 4.dp, + bottomEnd = 20.dp + ), + shadowElevation = 1.dp + ) { + Row( + modifier = Modifier.padding(horizontal = 20.dp, vertical = 16.dp), + horizontalArrangement = Arrangement.spacedBy(6.dp), + verticalAlignment = Alignment.CenterVertically + ) { + // Animated dots + repeat(3) { index -> + val infiniteTransition = rememberInfiniteTransition(label = "dot$index") + val scale by infiniteTransition.animateFloat( + initialValue = 0.5f, + targetValue = 1f, + animationSpec = infiniteRepeatable( + animation = tween(600, delayMillis = index * 150), + repeatMode = RepeatMode.Reverse + ), + label = "scale$index" + ) + + Box( + modifier = Modifier + .size(8.dp * scale) + .clip(CircleShape) + .background(TextSecondary.copy(alpha = 0.6f)) + ) + } + } } } } @@ -592,12 +627,19 @@ fun ChatBubble(message: ChatMessageEntity) { ), shadowElevation = if (!message.isUserMessage) 1.dp else 0.dp ) { - // === GANTI TEXT() MENJADI MarkdownText() DI SINI === - MarkdownText( - markdown = message.message, - textColor = if (message.isUserMessage) Color.White else TextPrimary, - modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp) - ) + if (message.isUserMessage) { + Text( + text = message.message, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp), + color = Color.White, + fontSize = 15.sp + ) + } else { + MarkdownText( + markdown = message.message, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp) + ) + } } Text( @@ -629,7 +671,6 @@ fun ChatBubble(message: ChatMessageEntity) { } } - @Composable fun SourcesTab( sources: List, @@ -727,7 +768,6 @@ fun SourceCard( .padding(16.dp), verticalAlignment = Alignment.CenterVertically ) { - // File icon dengan desain modern Box( modifier = Modifier .size(48.dp) @@ -807,4 +847,4 @@ fun SourceCard( } } } -} +} \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index 7353dbd..bbd3e02 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml index 7353dbd..bbd3e02 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file