History Chat AI berdasarkan Catatan yang ada didalam kategori dalam bentuk Drawer Menu di AI Helper
This commit is contained in:
parent
2037d32766
commit
2121682dd4
8
.idea/appInsightsSettings.xml
generated
8
.idea/appInsightsSettings.xml
generated
@ -6,14 +6,6 @@
|
||||
<entry key="Firebase Crashlytics">
|
||||
<value>
|
||||
<InsightsFilterSettings>
|
||||
<option name="connection">
|
||||
<ConnectionSetting>
|
||||
<option name="appId" value="PLACEHOLDER" />
|
||||
<option name="mobileSdkAppId" value="" />
|
||||
<option name="projectId" value="" />
|
||||
<option name="projectNumber" value="" />
|
||||
</ConnectionSetting>
|
||||
</option>
|
||||
<option name="signal" value="SIGNAL_UNSPECIFIED" />
|
||||
<option name="timeIntervalDays" value="THIRTY_DAYS" />
|
||||
<option name="visibilityType" value="ALL" />
|
||||
|
||||
@ -178,20 +178,22 @@ fun NotesApp() {
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
Scaffold(
|
||||
// Di MainActivity.kt, ubah bagian topBar menjadi:
|
||||
|
||||
topBar = {
|
||||
if (!showFullScreenNote) {
|
||||
// Hide TopBar untuk AI Helper screen dan FullScreen Note
|
||||
if (!showFullScreenNote && currentScreen != "ai") {
|
||||
ModernTopBar(
|
||||
title = when(currentScreen) {
|
||||
"main" -> if (selectedCategory != null) selectedCategory!!.name else "AI Notes"
|
||||
"ai" -> "AI Helper"
|
||||
"starred" -> "Berbintang"
|
||||
"archive" -> "Arsip"
|
||||
"trash" -> "Sampah"
|
||||
else -> "AI Notes"
|
||||
},
|
||||
showBackButton = (selectedCategory != null && currentScreen == "main") || currentScreen == "ai" || currentScreen == "starred",
|
||||
showBackButton = (selectedCategory != null && currentScreen == "main") || currentScreen == "starred",
|
||||
onBackClick = {
|
||||
if (currentScreen == "ai" || currentScreen == "starred") {
|
||||
if (currentScreen == "starred") {
|
||||
currentScreen = "main"
|
||||
} else {
|
||||
selectedCategory = null
|
||||
|
||||
@ -11,6 +11,7 @@ import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import com.example.notesai.data.model.Note
|
||||
import com.example.notesai.data.model.Category
|
||||
import com.example.notesai.data.model.ChatHistory
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.map
|
||||
@ -29,7 +30,7 @@ data class SerializableCategory(
|
||||
val gradientStart: Long,
|
||||
val gradientEnd: Long,
|
||||
val timestamp: Long,
|
||||
val isDeleted: Boolean = false // Support untuk soft delete
|
||||
val isDeleted: Boolean = false
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@ -37,7 +38,7 @@ data class SerializableNote(
|
||||
val id: String,
|
||||
val categoryId: String,
|
||||
val title: String,
|
||||
val description: String = "", // Field baru untuk v1.1.0
|
||||
val description: String = "",
|
||||
val content: String = "",
|
||||
val timestamp: Long,
|
||||
val isArchived: Boolean = false,
|
||||
@ -49,10 +50,11 @@ class DataStoreManager(private val context: Context) {
|
||||
companion object {
|
||||
val CATEGORIES_KEY = stringPreferencesKey("categories")
|
||||
val NOTES_KEY = stringPreferencesKey("notes")
|
||||
val CHAT_HISTORY_KEY = stringPreferencesKey("chat_history") // NEW
|
||||
}
|
||||
|
||||
private val json = Json {
|
||||
ignoreUnknownKeys = true // Penting untuk backward compatibility
|
||||
ignoreUnknownKeys = true
|
||||
encodeDefaults = true
|
||||
}
|
||||
|
||||
@ -99,7 +101,7 @@ class DataStoreManager(private val context: Context) {
|
||||
id = it.id,
|
||||
categoryId = it.categoryId,
|
||||
title = it.title,
|
||||
description = it.description, // Support field baru
|
||||
description = it.description,
|
||||
content = it.content,
|
||||
timestamp = it.timestamp,
|
||||
isPinned = it.isPinned,
|
||||
@ -113,6 +115,27 @@ class DataStoreManager(private val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// NEW: Chat History Flow
|
||||
val chatHistoryFlow: Flow<List<ChatHistory>> = context.dataStore.data
|
||||
.catch { exception ->
|
||||
if (exception is IOException) {
|
||||
emit(emptyPreferences())
|
||||
} else {
|
||||
throw exception
|
||||
}
|
||||
}
|
||||
.map { preferences ->
|
||||
val jsonString = preferences[CHAT_HISTORY_KEY] ?: "[]"
|
||||
try {
|
||||
json.decodeFromString<List<ChatHistory>>(jsonString)
|
||||
.filter { !it.isDeleted }
|
||||
.sortedByDescending { it.timestamp }
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun saveCategories(categories: List<Category>) {
|
||||
try {
|
||||
context.dataStore.edit { preferences ->
|
||||
@ -141,7 +164,7 @@ class DataStoreManager(private val context: Context) {
|
||||
id = it.id,
|
||||
categoryId = it.categoryId,
|
||||
title = it.title,
|
||||
description = it.description, // Support field baru
|
||||
description = it.description,
|
||||
content = it.content,
|
||||
timestamp = it.timestamp,
|
||||
isPinned = it.isPinned,
|
||||
@ -155,4 +178,45 @@ class DataStoreManager(private val context: Context) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
// NEW: Save Chat History
|
||||
suspend fun saveChatHistory(chatHistoryList: List<ChatHistory>) {
|
||||
try {
|
||||
context.dataStore.edit { preferences ->
|
||||
preferences[CHAT_HISTORY_KEY] = json.encodeToString(chatHistoryList)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
// NEW: Add new chat history
|
||||
suspend fun addChatHistory(chatHistory: ChatHistory) {
|
||||
try {
|
||||
context.dataStore.edit { preferences ->
|
||||
val jsonString = preferences[CHAT_HISTORY_KEY] ?: "[]"
|
||||
val currentList = json.decodeFromString<List<ChatHistory>>(jsonString).toMutableList()
|
||||
currentList.add(0, chatHistory) // Add to beginning
|
||||
preferences[CHAT_HISTORY_KEY] = json.encodeToString(currentList)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
// NEW: Delete chat history (soft delete)
|
||||
suspend fun deleteChatHistory(historyId: String) {
|
||||
try {
|
||||
context.dataStore.edit { preferences ->
|
||||
val jsonString = preferences[CHAT_HISTORY_KEY] ?: "[]"
|
||||
val currentList = json.decodeFromString<List<ChatHistory>>(jsonString).toMutableList()
|
||||
val updatedList = currentList.map {
|
||||
if (it.id == historyId) it.copy(isDeleted = true) else it
|
||||
}
|
||||
preferences[CHAT_HISTORY_KEY] = json.encodeToString(updatedList)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
package com.example.notesai.data.model
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.util.UUID
|
||||
|
||||
@Serializable
|
||||
data class ChatHistory(
|
||||
val id: String = UUID.randomUUID().toString(),
|
||||
val categoryId: String?, // null berarti "Semua Kategori"
|
||||
val categoryName: String, // Untuk display
|
||||
val messages: List<SerializableChatMessage>,
|
||||
val lastMessagePreview: String, // Preview pesan terakhir
|
||||
val timestamp: Long = System.currentTimeMillis(),
|
||||
val isDeleted: Boolean = false
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SerializableChatMessage(
|
||||
val id: String,
|
||||
val message: String,
|
||||
val isUser: Boolean,
|
||||
val timestamp: Long
|
||||
)
|
||||
|
||||
// Extension function untuk convert ChatMessage ke SerializableChatMessage
|
||||
fun ChatMessage.toSerializable() = SerializableChatMessage(
|
||||
id = id,
|
||||
message = message,
|
||||
isUser = isUser,
|
||||
timestamp = timestamp
|
||||
)
|
||||
|
||||
// Extension function untuk convert SerializableChatMessage ke ChatMessage
|
||||
fun SerializableChatMessage.toChatMessage() = ChatMessage(
|
||||
id = id,
|
||||
message = message,
|
||||
isUser = isUser,
|
||||
timestamp = timestamp
|
||||
)
|
||||
@ -0,0 +1,537 @@
|
||||
package com.example.notesai.presentation.screens.ai.components
|
||||
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.example.notesai.data.model.ChatHistory
|
||||
import com.example.notesai.data.model.Category
|
||||
import com.example.notesai.data.model.Note
|
||||
import com.example.notesai.util.Constants
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
@Composable
|
||||
fun ChatHistoryDrawer(
|
||||
chatHistories: List<ChatHistory>,
|
||||
categories: List<Category>,
|
||||
notes: List<Note>,
|
||||
selectedCategory: Category?,
|
||||
onDismiss: () -> Unit,
|
||||
onHistoryClick: (ChatHistory) -> Unit,
|
||||
onDeleteHistory: (String) -> Unit,
|
||||
onCategorySelected: (Category?) -> Unit,
|
||||
onNewChat: () -> Unit
|
||||
) {
|
||||
var showCategoryDropdown by remember { mutableStateOf(false) }
|
||||
|
||||
// Filter categories that have notes
|
||||
val categoriesWithNotes = categories.filter { category ->
|
||||
notes.any { note -> note.categoryId == category.id && !note.isArchived && !note.isDeleted }
|
||||
}
|
||||
|
||||
// Group histories by category
|
||||
val groupedHistories = chatHistories.groupBy { it.categoryId to it.categoryName }
|
||||
|
||||
// Backdrop with blur effect
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Constants.AppColors.Overlay)
|
||||
.clickable(
|
||||
onClick = onDismiss,
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
)
|
||||
) {
|
||||
// Drawer Content
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.width(320.dp)
|
||||
.align(Alignment.CenterStart)
|
||||
.clickable(
|
||||
onClick = {},
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
),
|
||||
color = Constants.AppColors.Surface,
|
||||
shadowElevation = Constants.Elevation.ExtraLarge.dp
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
// Header
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
brush = Brush.verticalGradient(
|
||||
colors = listOf(
|
||||
Constants.AppColors.Primary.copy(alpha = 0.15f),
|
||||
Color.Transparent
|
||||
)
|
||||
)
|
||||
)
|
||||
.padding(Constants.Spacing.Large.dp)
|
||||
) {
|
||||
Column {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.background(
|
||||
color = Constants.AppColors.Primary.copy(alpha = 0.2f),
|
||||
shape = RoundedCornerShape(Constants.Radius.Medium.dp)
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.History,
|
||||
contentDescription = null,
|
||||
tint = Constants.AppColors.Primary,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
"Riwayat Chat",
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
color = Constants.AppColors.OnBackground,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
"${chatHistories.size} percakapan tersimpan",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = Constants.AppColors.OnSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(Constants.Spacing.Medium.dp))
|
||||
|
||||
// Category Selector
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = Constants.Spacing.Large.dp)
|
||||
) {
|
||||
Text(
|
||||
"Filter Kategori",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = Constants.AppColors.OnSurfaceTertiary,
|
||||
fontSize = 12.sp
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Box {
|
||||
Card(
|
||||
onClick = { showCategoryDropdown = !showCategoryDropdown },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = Constants.AppColors.SurfaceVariant
|
||||
),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(12.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Folder,
|
||||
contentDescription = null,
|
||||
tint = Constants.AppColors.Primary,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
Text(
|
||||
selectedCategory?.name ?: "Semua Kategori",
|
||||
color = Constants.AppColors.OnSurface,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 14.sp
|
||||
)
|
||||
}
|
||||
Icon(
|
||||
Icons.Default.ArrowDropDown,
|
||||
contentDescription = null,
|
||||
tint = Constants.AppColors.OnSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
DropdownMenu(
|
||||
expanded = showCategoryDropdown,
|
||||
onDismissRequest = { showCategoryDropdown = false },
|
||||
modifier = Modifier
|
||||
.width(280.dp)
|
||||
.background(Constants.AppColors.SurfaceElevated)
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Folder,
|
||||
contentDescription = null,
|
||||
tint = Constants.AppColors.OnSurfaceVariant,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
Text(
|
||||
"Semua Kategori",
|
||||
color = Constants.AppColors.OnSurface
|
||||
)
|
||||
}
|
||||
},
|
||||
onClick = {
|
||||
onCategorySelected(null)
|
||||
showCategoryDropdown = false
|
||||
onNewChat()
|
||||
},
|
||||
leadingIcon = {
|
||||
if (selectedCategory == null) {
|
||||
Icon(
|
||||
Icons.Default.Check,
|
||||
contentDescription = null,
|
||||
tint = Constants.AppColors.Primary,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (categoriesWithNotes.isNotEmpty()) {
|
||||
HorizontalDivider(color = Constants.AppColors.Divider)
|
||||
}
|
||||
|
||||
categoriesWithNotes.forEach { category ->
|
||||
val notesCount = notes.count {
|
||||
it.categoryId == category.id && !it.isArchived && !it.isDeleted
|
||||
}
|
||||
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Folder,
|
||||
contentDescription = null,
|
||||
tint = Color(category.gradientStart),
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
Text(
|
||||
category.name,
|
||||
color = Constants.AppColors.OnSurface
|
||||
)
|
||||
}
|
||||
|
||||
Surface(
|
||||
color = Constants.AppColors.Primary.copy(alpha = 0.15f),
|
||||
shape = RoundedCornerShape(Constants.Radius.Small.dp)
|
||||
) {
|
||||
Text(
|
||||
notesCount.toString(),
|
||||
modifier = Modifier.padding(
|
||||
horizontal = 8.dp,
|
||||
vertical = 2.dp
|
||||
),
|
||||
color = Constants.AppColors.Primary,
|
||||
fontSize = 11.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
onClick = {
|
||||
onCategorySelected(category)
|
||||
showCategoryDropdown = false
|
||||
onNewChat()
|
||||
},
|
||||
leadingIcon = {
|
||||
if (selectedCategory?.id == category.id) {
|
||||
Icon(
|
||||
Icons.Default.Check,
|
||||
contentDescription = null,
|
||||
tint = Constants.AppColors.Primary,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(Constants.Spacing.Medium.dp))
|
||||
|
||||
HorizontalDivider(
|
||||
color = Constants.AppColors.Divider,
|
||||
modifier = Modifier.padding(horizontal = Constants.Spacing.Large.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(Constants.Spacing.Small.dp))
|
||||
|
||||
// Chat History List
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f),
|
||||
contentPadding = PaddingValues(Constants.Spacing.Medium.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(Constants.Spacing.Medium.dp)
|
||||
) {
|
||||
if (chatHistories.isEmpty()) {
|
||||
item {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(Constants.Spacing.ExtraLarge.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.ChatBubbleOutline,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(48.dp),
|
||||
tint = Constants.AppColors.OnSurfaceVariant
|
||||
)
|
||||
Text(
|
||||
"Belum ada riwayat chat",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = Constants.AppColors.OnSurfaceVariant,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
Text(
|
||||
"Mulai chat dengan AI",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = Constants.AppColors.OnSurfaceTertiary
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Group by category
|
||||
groupedHistories.forEach { (categoryInfo, histories) ->
|
||||
item {
|
||||
// Category Header
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
horizontal = Constants.Spacing.Small.dp,
|
||||
vertical = Constants.Spacing.ExtraSmall.dp
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Folder,
|
||||
contentDescription = null,
|
||||
tint = Constants.AppColors.Primary,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
Text(
|
||||
categoryInfo.second,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = Constants.AppColors.OnSurfaceVariant,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 13.sp
|
||||
)
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.weight(1f),
|
||||
color = Constants.AppColors.Divider
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
items(histories) { history ->
|
||||
ChatHistoryItem(
|
||||
history = history,
|
||||
onClick = { onHistoryClick(history) },
|
||||
onDelete = { onDeleteHistory(history.id) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChatHistoryItem(
|
||||
history: ChatHistory,
|
||||
onClick: () -> Unit,
|
||||
onDelete: () -> Unit
|
||||
) {
|
||||
var showDeleteConfirm by remember { mutableStateOf(false) }
|
||||
val dateFormat = remember { SimpleDateFormat("dd MMM, HH:mm", Locale("id", "ID")) }
|
||||
|
||||
Card(
|
||||
onClick = onClick,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = Constants.AppColors.SurfaceVariant
|
||||
),
|
||||
shape = RoundedCornerShape(Constants.Radius.Medium.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(Constants.Spacing.Medium.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// Message Count
|
||||
Surface(
|
||||
color = Constants.AppColors.Primary.copy(alpha = 0.15f),
|
||||
shape = RoundedCornerShape(Constants.Radius.Small.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(
|
||||
horizontal = Constants.Spacing.Small.dp,
|
||||
vertical = 4.dp
|
||||
),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Chat,
|
||||
contentDescription = null,
|
||||
tint = Constants.AppColors.Primary,
|
||||
modifier = Modifier.size(12.dp)
|
||||
)
|
||||
Text(
|
||||
"${history.messages.size}",
|
||||
color = Constants.AppColors.Primary,
|
||||
fontSize = 11.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Delete Button
|
||||
IconButton(
|
||||
onClick = { showDeleteConfirm = true },
|
||||
modifier = Modifier.size(32.dp)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Delete,
|
||||
contentDescription = "Delete",
|
||||
tint = Constants.AppColors.Error,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
// Preview
|
||||
Text(
|
||||
history.lastMessagePreview,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = Constants.AppColors.OnSurface,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
fontSize = 14.sp
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
// Timestamp
|
||||
Text(
|
||||
dateFormat.format(Date(history.timestamp)),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = Constants.AppColors.OnSurfaceTertiary,
|
||||
fontSize = 12.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Delete Confirmation Dialog
|
||||
if (showDeleteConfirm) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showDeleteConfirm = false },
|
||||
icon = {
|
||||
Icon(
|
||||
Icons.Default.DeleteForever,
|
||||
contentDescription = null,
|
||||
tint = Constants.AppColors.Error
|
||||
)
|
||||
},
|
||||
title = {
|
||||
Text(
|
||||
"Hapus Riwayat Chat?",
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Text("Riwayat chat ini akan dihapus permanen dan tidak dapat dikembalikan.")
|
||||
},
|
||||
confirmButton = {
|
||||
Button(
|
||||
onClick = {
|
||||
onDelete()
|
||||
showDeleteConfirm = false
|
||||
},
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Constants.AppColors.Error
|
||||
)
|
||||
) {
|
||||
Text("Hapus")
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showDeleteConfirm = false }) {
|
||||
Text("Batal")
|
||||
}
|
||||
},
|
||||
containerColor = Constants.AppColors.SurfaceElevated
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -14,28 +14,26 @@ import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.example.notesai.data.model.Category
|
||||
import com.example.notesai.data.model.ChatMessage
|
||||
import com.example.notesai.data.model.Note
|
||||
import com.example.notesai.data.local.DataStoreManager
|
||||
import com.example.notesai.data.model.*
|
||||
import com.example.notesai.util.Constants
|
||||
import com.google.ai.client.generativeai.GenerativeModel
|
||||
import com.google.ai.client.generativeai.type.generationConfig
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import com.example.notesai.presentation.screens.ai.components.ChatBubble
|
||||
import com.example.notesai.presentation.screens.ai.components.ChatHistoryDrawer
|
||||
import com.example.notesai.presentation.screens.ai.components.CompactStatItem
|
||||
import com.example.notesai.presentation.screens.ai.components.SuggestionChip
|
||||
import com.example.notesai.presentation.screens.ai.components.StatItem
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@ -43,19 +41,26 @@ fun AIHelperScreen(
|
||||
categories: List<Category>,
|
||||
notes: List<Note>
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val dataStoreManager = remember { DataStoreManager(context) }
|
||||
|
||||
var prompt by remember { mutableStateOf("") }
|
||||
var isLoading by remember { mutableStateOf(false) }
|
||||
var selectedCategory by remember { mutableStateOf<Category?>(null) }
|
||||
var showCategoryDropdown by remember { mutableStateOf(false) }
|
||||
var errorMessage by remember { mutableStateOf("") }
|
||||
var chatMessages by remember { mutableStateOf(listOf<ChatMessage>()) }
|
||||
var showCopiedMessage by remember { mutableStateOf(false) }
|
||||
var copiedMessageId by remember { mutableStateOf("") }
|
||||
var showHistoryDrawer by remember { mutableStateOf(false) }
|
||||
var currentChatId by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
// Load chat histories
|
||||
val chatHistories by dataStoreManager.chatHistoryFlow.collectAsState(initial = emptyList())
|
||||
|
||||
// Inisialisasi Gemini Model
|
||||
val generativeModel = remember {
|
||||
GenerativeModel(
|
||||
@ -79,378 +84,460 @@ fun AIHelperScreen(
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Constants.AppColors.Background)
|
||||
) {
|
||||
// Category Selector & Stats - Compact
|
||||
Surface(
|
||||
color = Constants.AppColors.Surface,
|
||||
shadowElevation = 2.dp
|
||||
// Function to save chat history
|
||||
fun saveChatHistory() {
|
||||
if (chatMessages.isNotEmpty()) {
|
||||
scope.launch {
|
||||
val lastMessage = chatMessages.lastOrNull()?.message ?: ""
|
||||
val preview = if (lastMessage.length > 100) {
|
||||
lastMessage.take(100) + "..."
|
||||
} else lastMessage
|
||||
|
||||
val chatHistory = ChatHistory(
|
||||
id = currentChatId ?: UUID.randomUUID().toString(),
|
||||
categoryId = selectedCategory?.id,
|
||||
categoryName = selectedCategory?.name ?: "Semua Kategori",
|
||||
messages = chatMessages.map { it.toSerializable() },
|
||||
lastMessagePreview = preview,
|
||||
timestamp = System.currentTimeMillis()
|
||||
)
|
||||
|
||||
dataStoreManager.addChatHistory(chatHistory)
|
||||
currentChatId = chatHistory.id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Function to load chat history
|
||||
fun loadChatHistory(history: ChatHistory) {
|
||||
chatMessages = history.messages.map { it.toChatMessage() }
|
||||
currentChatId = history.id
|
||||
selectedCategory = categories.find { it.id == history.categoryId }
|
||||
showHistoryDrawer = false
|
||||
}
|
||||
|
||||
// Function to start new chat
|
||||
fun startNewChat() {
|
||||
chatMessages = emptyList()
|
||||
currentChatId = null
|
||||
errorMessage = ""
|
||||
showHistoryDrawer = false
|
||||
}
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Constants.AppColors.Background)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
// Top Bar with History Button & Stats
|
||||
Surface(
|
||||
color = Constants.AppColors.Surface,
|
||||
shadowElevation = 2.dp
|
||||
) {
|
||||
// Category Selector
|
||||
Box {
|
||||
Card(
|
||||
onClick = { showCategoryDropdown = !showCategoryDropdown },
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
// Top Row - Menu & New Chat
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = Constants.AppColors.SurfaceVariant
|
||||
),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(
|
||||
// History Drawer Button
|
||||
IconButton(
|
||||
onClick = { showHistoryDrawer = true },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(12.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
.size(40.dp)
|
||||
.background(
|
||||
Constants.AppColors.Primary.copy(alpha = 0.1f),
|
||||
CircleShape
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Menu,
|
||||
contentDescription = "Menu",
|
||||
tint = Constants.AppColors.Primary,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
}
|
||||
|
||||
// Category Badge
|
||||
Surface(
|
||||
color = Constants.AppColors.Primary.copy(alpha = 0.1f),
|
||||
shape = RoundedCornerShape(Constants.Radius.Medium.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
modifier = Modifier.padding(
|
||||
horizontal = Constants.Spacing.Medium.dp,
|
||||
vertical = Constants.Spacing.Small.dp
|
||||
),
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Folder,
|
||||
contentDescription = null,
|
||||
tint = Constants.AppColors.Primary,
|
||||
modifier = Modifier.size(20.dp)
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
Text(
|
||||
selectedCategory?.name ?: "Semua Kategori",
|
||||
color = Constants.AppColors.OnSurface,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.Medium
|
||||
color = Constants.AppColors.Primary,
|
||||
fontSize = 13.sp,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
Icon(
|
||||
Icons.Default.ArrowDropDown,
|
||||
contentDescription = null,
|
||||
tint = Constants.AppColors.OnSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
DropdownMenu(
|
||||
expanded = showCategoryDropdown,
|
||||
onDismissRequest = { showCategoryDropdown = false },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(0.9f)
|
||||
.background(Constants.AppColors.SurfaceElevated)
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("Semua Kategori", color = Constants.AppColors.OnSurface) },
|
||||
onClick = {
|
||||
selectedCategory = null
|
||||
showCategoryDropdown = false
|
||||
}
|
||||
)
|
||||
categories.forEach { category ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(category.name, color = Constants.AppColors.OnSurface) },
|
||||
onClick = {
|
||||
selectedCategory = category
|
||||
showCategoryDropdown = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
// Stats - Compact
|
||||
val filteredNotes = if (selectedCategory != null) {
|
||||
notes.filter { it.categoryId == selectedCategory!!.id && !it.isArchived }
|
||||
} else {
|
||||
notes.filter { !it.isArchived }
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
CompactStatItem(
|
||||
icon = Icons.Default.Description,
|
||||
value = filteredNotes.size.toString(),
|
||||
label = "Catatan"
|
||||
)
|
||||
CompactStatItem(
|
||||
icon = Icons.Default.Star,
|
||||
value = filteredNotes.count { it.isPinned }.toString(),
|
||||
label = "Dipasang"
|
||||
)
|
||||
CompactStatItem(
|
||||
icon = Icons.Default.Folder,
|
||||
value = categories.size.toString(),
|
||||
label = "Kategori"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalDivider(color = Constants.AppColors.Divider)
|
||||
|
||||
// Chat Area
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
if (chatMessages.isEmpty()) {
|
||||
// Welcome State
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(32.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(80.dp)
|
||||
.background(
|
||||
color = Constants.AppColors.Primary.copy(alpha = 0.1f),
|
||||
shape = CircleShape
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.AutoAwesome,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(40.dp),
|
||||
tint = Constants.AppColors.Primary
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
Text(
|
||||
"AI Assistant",
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
color = Constants.AppColors.OnBackground,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
"Tanyakan apa saja tentang catatan Anda",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = Constants.AppColors.OnSurfaceVariant,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
// Suggestion Chips
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier.fillMaxWidth(0.85f)
|
||||
) {
|
||||
Text(
|
||||
"Contoh pertanyaan:",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = Constants.AppColors.OnSurfaceTertiary
|
||||
)
|
||||
SuggestionChip("Analisis catatan saya") { prompt = it }
|
||||
SuggestionChip("Buat ringkasan") { prompt = it }
|
||||
SuggestionChip("Berikan saran organisasi") { prompt = it }
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Chat Messages
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(scrollState)
|
||||
.padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 100.dp)
|
||||
) {
|
||||
chatMessages.forEach { message ->
|
||||
ChatBubble(
|
||||
message = message,
|
||||
onCopy = {
|
||||
clipboardManager.setText(AnnotatedString(message.message))
|
||||
copiedMessageId = message.id
|
||||
showCopiedMessage = true
|
||||
scope.launch {
|
||||
delay(2000)
|
||||
showCopiedMessage = false
|
||||
}
|
||||
},
|
||||
showCopied = showCopiedMessage && copiedMessageId == message.id
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
}
|
||||
|
||||
// Loading Indicator
|
||||
if (isLoading) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp),
|
||||
horizontalArrangement = Arrangement.Start
|
||||
) {
|
||||
Surface(
|
||||
color = Constants.AppColors.SurfaceVariant,
|
||||
shape = RoundedCornerShape(16.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(20.dp),
|
||||
color = Constants.AppColors.Primary,
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
Text(
|
||||
"AI sedang berpikir...",
|
||||
color = Constants.AppColors.OnSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Error Message
|
||||
if (errorMessage.isNotEmpty()) {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
color = Constants.AppColors.Error.copy(alpha = 0.1f),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
// New Chat Button
|
||||
if (chatMessages.isNotEmpty()) {
|
||||
Button(
|
||||
onClick = { startNewChat() },
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Constants.AppColors.Primary
|
||||
),
|
||||
shape = RoundedCornerShape(Constants.Radius.Medium.dp),
|
||||
contentPadding = PaddingValues(
|
||||
horizontal = Constants.Spacing.Medium.dp,
|
||||
vertical = Constants.Spacing.Small.dp
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Warning,
|
||||
Icons.Default.Add,
|
||||
contentDescription = null,
|
||||
tint = Constants.AppColors.Error,
|
||||
modifier = Modifier.size(20.dp)
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
Text(
|
||||
errorMessage,
|
||||
color = Constants.AppColors.Error,
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
"Baru",
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
// Stats - Compact
|
||||
val filteredNotes = if (selectedCategory != null) {
|
||||
notes.filter { it.categoryId == selectedCategory!!.id && !it.isArchived }
|
||||
} else {
|
||||
notes.filter { !it.isArchived }
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
CompactStatItem(
|
||||
icon = Icons.Default.Description,
|
||||
value = filteredNotes.size.toString(),
|
||||
label = "Catatan"
|
||||
)
|
||||
CompactStatItem(
|
||||
icon = Icons.Default.Star,
|
||||
value = filteredNotes.count { it.isPinned }.toString(),
|
||||
label = "Dipasang"
|
||||
)
|
||||
CompactStatItem(
|
||||
icon = Icons.Default.Folder,
|
||||
value = categories.size.toString(),
|
||||
label = "Kategori"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalDivider(color = Constants.AppColors.Divider)
|
||||
|
||||
// Chat Area
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
if (chatMessages.isEmpty()) {
|
||||
// Welcome State
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(32.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(80.dp)
|
||||
.background(
|
||||
color = Constants.AppColors.Primary.copy(alpha = 0.1f),
|
||||
shape = CircleShape
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.AutoAwesome,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(40.dp),
|
||||
tint = Constants.AppColors.Primary
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
Text(
|
||||
"AI Assistant",
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
color = Constants.AppColors.OnBackground,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
"Tanyakan apa saja tentang catatan Anda",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = Constants.AppColors.OnSurfaceVariant,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
// Suggestion Chips
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier.fillMaxWidth(0.85f)
|
||||
) {
|
||||
Text(
|
||||
"Contoh pertanyaan:",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = Constants.AppColors.OnSurfaceTertiary
|
||||
)
|
||||
SuggestionChip("Analisis catatan saya") { prompt = it }
|
||||
SuggestionChip("Buat ringkasan") { prompt = it }
|
||||
SuggestionChip("Berikan saran organisasi") { prompt = it }
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Chat Messages
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(scrollState)
|
||||
.padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 100.dp)
|
||||
) {
|
||||
chatMessages.forEach { message ->
|
||||
ChatBubble(
|
||||
message = message,
|
||||
onCopy = {
|
||||
clipboardManager.setText(AnnotatedString(message.message))
|
||||
copiedMessageId = message.id
|
||||
showCopiedMessage = true
|
||||
scope.launch {
|
||||
delay(2000)
|
||||
showCopiedMessage = false
|
||||
}
|
||||
},
|
||||
showCopied = showCopiedMessage && copiedMessageId == message.id
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
}
|
||||
|
||||
// Loading Indicator
|
||||
if (isLoading) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp),
|
||||
horizontalArrangement = Arrangement.Start
|
||||
) {
|
||||
Surface(
|
||||
color = Constants.AppColors.SurfaceVariant,
|
||||
shape = RoundedCornerShape(16.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(20.dp),
|
||||
color = Constants.AppColors.Primary,
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
Text(
|
||||
"AI sedang berpikir...",
|
||||
color = Constants.AppColors.OnSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Error Message
|
||||
if (errorMessage.isNotEmpty()) {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
color = Constants.AppColors.Error.copy(alpha = 0.1f),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Warning,
|
||||
contentDescription = null,
|
||||
tint = Constants.AppColors.Error,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
Text(
|
||||
errorMessage,
|
||||
color = Constants.AppColors.Error,
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Input Area - Minimalist
|
||||
Surface(
|
||||
color = Constants.AppColors.Surface,
|
||||
shadowElevation = 8.dp,
|
||||
shape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
verticalAlignment = Alignment.Bottom,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
// Input Area - Minimalist
|
||||
Surface(
|
||||
color = Constants.AppColors.Surface,
|
||||
shadowElevation = 8.dp,
|
||||
shape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp)
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = prompt,
|
||||
onValueChange = { prompt = it },
|
||||
placeholder = {
|
||||
Text(
|
||||
"Ketik pesan...",
|
||||
color = Constants.AppColors.OnSurfaceTertiary
|
||||
)
|
||||
},
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.heightIn(min = 48.dp, max = 120.dp),
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedTextColor = Constants.AppColors.OnBackground,
|
||||
unfocusedTextColor = Constants.AppColors.OnSurface,
|
||||
focusedContainerColor = Constants.AppColors.SurfaceVariant,
|
||||
unfocusedContainerColor = Constants.AppColors.SurfaceVariant,
|
||||
cursorColor = Constants.AppColors.Primary,
|
||||
focusedBorderColor = Constants.AppColors.Primary,
|
||||
unfocusedBorderColor = Color.Transparent
|
||||
),
|
||||
shape = RoundedCornerShape(24.dp),
|
||||
maxLines = 4
|
||||
)
|
||||
|
||||
// Send Button
|
||||
FloatingActionButton(
|
||||
onClick = {
|
||||
if (prompt.isNotBlank() && !isLoading) {
|
||||
scope.launch {
|
||||
chatMessages = chatMessages + ChatMessage(
|
||||
message = prompt,
|
||||
isUser = true
|
||||
)
|
||||
|
||||
val userPrompt = prompt
|
||||
prompt = ""
|
||||
isLoading = true
|
||||
errorMessage = ""
|
||||
|
||||
try {
|
||||
val filteredNotes = if (selectedCategory != null) {
|
||||
notes.filter { it.categoryId == selectedCategory!!.id && !it.isArchived }
|
||||
} else {
|
||||
notes.filter { !it.isArchived }
|
||||
}
|
||||
|
||||
val notesContext = buildString {
|
||||
appendLine("Data catatan pengguna:")
|
||||
appendLine("Total catatan: ${filteredNotes.size}")
|
||||
appendLine("Kategori: ${selectedCategory?.name ?: "Semua"}")
|
||||
appendLine()
|
||||
appendLine("Daftar catatan:")
|
||||
filteredNotes.take(10).forEach { note ->
|
||||
appendLine("- Judul: ${note.title}")
|
||||
appendLine(" Isi: ${note.content.take(100)}")
|
||||
appendLine()
|
||||
}
|
||||
}
|
||||
|
||||
val fullPrompt = "$notesContext\n\nPertanyaan: $userPrompt\n\nBerikan jawaban dalam bahasa Indonesia yang natural dan membantu."
|
||||
val result = generativeModel.generateContent(fullPrompt)
|
||||
val response = result.text ?: "Tidak ada respons dari AI"
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
verticalAlignment = Alignment.Bottom,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = prompt,
|
||||
onValueChange = { prompt = it },
|
||||
placeholder = {
|
||||
Text(
|
||||
"Ketik pesan...",
|
||||
color = Constants.AppColors.OnSurfaceTertiary
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.heightIn(min = 48.dp, max = 120.dp),
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedTextColor = Constants.AppColors.OnBackground,
|
||||
unfocusedTextColor = Constants.AppColors.OnSurface,
|
||||
focusedContainerColor = Constants.AppColors.SurfaceVariant,
|
||||
unfocusedContainerColor = Constants.AppColors.SurfaceVariant,
|
||||
cursorColor = Constants.AppColors.Primary,
|
||||
focusedBorderColor = Constants.AppColors.Primary,
|
||||
unfocusedBorderColor = Color.Transparent
|
||||
),
|
||||
shape = RoundedCornerShape(24.dp),
|
||||
maxLines = 4
|
||||
)
|
||||
|
||||
// Send Button
|
||||
FloatingActionButton(
|
||||
onClick = {
|
||||
if (prompt.isNotBlank() && !isLoading) {
|
||||
scope.launch {
|
||||
chatMessages = chatMessages + ChatMessage(
|
||||
message = response,
|
||||
isUser = false
|
||||
message = prompt,
|
||||
isUser = true
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
errorMessage = "Error: ${e.message}"
|
||||
} finally {
|
||||
isLoading = false
|
||||
|
||||
val userPrompt = prompt
|
||||
prompt = ""
|
||||
isLoading = true
|
||||
errorMessage = ""
|
||||
|
||||
try {
|
||||
val filteredNotes = if (selectedCategory != null) {
|
||||
notes.filter { it.categoryId == selectedCategory!!.id && !it.isArchived }
|
||||
} else {
|
||||
notes.filter { !it.isArchived }
|
||||
}
|
||||
|
||||
val notesContext = buildString {
|
||||
appendLine("Data catatan pengguna:")
|
||||
appendLine("Total catatan: ${filteredNotes.size}")
|
||||
appendLine("Kategori: ${selectedCategory?.name ?: "Semua"}")
|
||||
appendLine()
|
||||
appendLine("Daftar catatan:")
|
||||
filteredNotes.take(10).forEach { note ->
|
||||
appendLine("- Judul: ${note.title}")
|
||||
appendLine(" Isi: ${note.content.take(100)}")
|
||||
appendLine()
|
||||
}
|
||||
}
|
||||
|
||||
val fullPrompt = "$notesContext\n\nPertanyaan: $userPrompt\n\nBerikan jawaban dalam bahasa Indonesia yang natural dan membantu."
|
||||
val result = generativeModel.generateContent(fullPrompt)
|
||||
val response = result.text ?: "Tidak ada respons dari AI"
|
||||
|
||||
chatMessages = chatMessages + ChatMessage(
|
||||
message = response,
|
||||
isUser = false
|
||||
)
|
||||
|
||||
// Auto-save chat history
|
||||
saveChatHistory()
|
||||
} catch (e: Exception) {
|
||||
errorMessage = "Error: ${e.message}"
|
||||
} finally {
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
containerColor = Constants.AppColors.Primary,
|
||||
modifier = Modifier.size(48.dp)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Send,
|
||||
contentDescription = "Send",
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
},
|
||||
containerColor = Constants.AppColors.Primary,
|
||||
modifier = Modifier.size(48.dp)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Send,
|
||||
contentDescription = "Send",
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Chat History Drawer
|
||||
AnimatedVisibility(
|
||||
visible = showHistoryDrawer,
|
||||
enter = fadeIn() + slideInHorizontally(),
|
||||
exit = fadeOut() + slideOutHorizontally()
|
||||
) {
|
||||
ChatHistoryDrawer(
|
||||
chatHistories = chatHistories,
|
||||
categories = categories,
|
||||
notes = notes,
|
||||
selectedCategory = selectedCategory,
|
||||
onDismiss = { showHistoryDrawer = false },
|
||||
onHistoryClick = { loadChatHistory(it) },
|
||||
onDeleteHistory = { historyId ->
|
||||
scope.launch {
|
||||
dataStoreManager.deleteChatHistory(historyId)
|
||||
}
|
||||
},
|
||||
onCategorySelected = { category ->
|
||||
selectedCategory = category
|
||||
},
|
||||
onNewChat = { startNewChat() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -5,7 +5,7 @@ import androidx.compose.ui.graphics.Color
|
||||
object Constants {
|
||||
// App Info
|
||||
const val APP_NAME = "NotesAI"
|
||||
const val APP_VERSION = "1.0.0"
|
||||
const val APP_VERSION = "1.1.0"
|
||||
|
||||
// DataStore
|
||||
const val DATASTORE_NAME = "notes_prefs"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user