Typing Indicator dan Auto Scroll ke Chat Terbaru
This commit is contained in:
parent
ad5aaefdc4
commit
53472b2768
@ -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<ChatMessageEntity>,
|
||||
sources: List<SourceEntity>,
|
||||
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,13 +627,20 @@ fun ChatBubble(message: ChatMessageEntity) {
|
||||
),
|
||||
shadowElevation = if (!message.isUserMessage) 1.dp else 0.dp
|
||||
) {
|
||||
// === GANTI TEXT() MENJADI MarkdownText() DI SINI ===
|
||||
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,
|
||||
textColor = if (message.isUserMessage) Color.White else TextPrimary,
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
text = dateFormat.format(Date(message.timestamp)),
|
||||
@ -629,7 +671,6 @@ fun ChatBubble(message: ChatMessageEntity) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun SourcesTab(
|
||||
sources: List<SourceEntity>,
|
||||
@ -727,7 +768,6 @@ fun SourceCard(
|
||||
.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// File icon dengan desain modern
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
Loading…
x
Reference in New Issue
Block a user