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">
|
<entry key="Firebase Crashlytics">
|
||||||
<value>
|
<value>
|
||||||
<InsightsFilterSettings>
|
<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="signal" value="SIGNAL_UNSPECIFIED" />
|
||||||
<option name="timeIntervalDays" value="THIRTY_DAYS" />
|
<option name="timeIntervalDays" value="THIRTY_DAYS" />
|
||||||
<option name="visibilityType" value="ALL" />
|
<option name="visibilityType" value="ALL" />
|
||||||
|
|||||||
@ -178,20 +178,22 @@ fun NotesApp() {
|
|||||||
|
|
||||||
Box(modifier = Modifier.fillMaxSize()) {
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
|
// Di MainActivity.kt, ubah bagian topBar menjadi:
|
||||||
|
|
||||||
topBar = {
|
topBar = {
|
||||||
if (!showFullScreenNote) {
|
// Hide TopBar untuk AI Helper screen dan FullScreen Note
|
||||||
|
if (!showFullScreenNote && currentScreen != "ai") {
|
||||||
ModernTopBar(
|
ModernTopBar(
|
||||||
title = when(currentScreen) {
|
title = when(currentScreen) {
|
||||||
"main" -> if (selectedCategory != null) selectedCategory!!.name else "AI Notes"
|
"main" -> if (selectedCategory != null) selectedCategory!!.name else "AI Notes"
|
||||||
"ai" -> "AI Helper"
|
|
||||||
"starred" -> "Berbintang"
|
"starred" -> "Berbintang"
|
||||||
"archive" -> "Arsip"
|
"archive" -> "Arsip"
|
||||||
"trash" -> "Sampah"
|
"trash" -> "Sampah"
|
||||||
else -> "AI Notes"
|
else -> "AI Notes"
|
||||||
},
|
},
|
||||||
showBackButton = (selectedCategory != null && currentScreen == "main") || currentScreen == "ai" || currentScreen == "starred",
|
showBackButton = (selectedCategory != null && currentScreen == "main") || currentScreen == "starred",
|
||||||
onBackClick = {
|
onBackClick = {
|
||||||
if (currentScreen == "ai" || currentScreen == "starred") {
|
if (currentScreen == "starred") {
|
||||||
currentScreen = "main"
|
currentScreen = "main"
|
||||||
} else {
|
} else {
|
||||||
selectedCategory = null
|
selectedCategory = null
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import androidx.datastore.preferences.core.stringPreferencesKey
|
|||||||
import androidx.datastore.preferences.preferencesDataStore
|
import androidx.datastore.preferences.preferencesDataStore
|
||||||
import com.example.notesai.data.model.Note
|
import com.example.notesai.data.model.Note
|
||||||
import com.example.notesai.data.model.Category
|
import com.example.notesai.data.model.Category
|
||||||
|
import com.example.notesai.data.model.ChatHistory
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.catch
|
import kotlinx.coroutines.flow.catch
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
@ -29,7 +30,7 @@ data class SerializableCategory(
|
|||||||
val gradientStart: Long,
|
val gradientStart: Long,
|
||||||
val gradientEnd: Long,
|
val gradientEnd: Long,
|
||||||
val timestamp: Long,
|
val timestamp: Long,
|
||||||
val isDeleted: Boolean = false // Support untuk soft delete
|
val isDeleted: Boolean = false
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@ -37,7 +38,7 @@ data class SerializableNote(
|
|||||||
val id: String,
|
val id: String,
|
||||||
val categoryId: String,
|
val categoryId: String,
|
||||||
val title: String,
|
val title: String,
|
||||||
val description: String = "", // Field baru untuk v1.1.0
|
val description: String = "",
|
||||||
val content: String = "",
|
val content: String = "",
|
||||||
val timestamp: Long,
|
val timestamp: Long,
|
||||||
val isArchived: Boolean = false,
|
val isArchived: Boolean = false,
|
||||||
@ -49,10 +50,11 @@ class DataStoreManager(private val context: Context) {
|
|||||||
companion object {
|
companion object {
|
||||||
val CATEGORIES_KEY = stringPreferencesKey("categories")
|
val CATEGORIES_KEY = stringPreferencesKey("categories")
|
||||||
val NOTES_KEY = stringPreferencesKey("notes")
|
val NOTES_KEY = stringPreferencesKey("notes")
|
||||||
|
val CHAT_HISTORY_KEY = stringPreferencesKey("chat_history") // NEW
|
||||||
}
|
}
|
||||||
|
|
||||||
private val json = Json {
|
private val json = Json {
|
||||||
ignoreUnknownKeys = true // Penting untuk backward compatibility
|
ignoreUnknownKeys = true
|
||||||
encodeDefaults = true
|
encodeDefaults = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,7 +101,7 @@ class DataStoreManager(private val context: Context) {
|
|||||||
id = it.id,
|
id = it.id,
|
||||||
categoryId = it.categoryId,
|
categoryId = it.categoryId,
|
||||||
title = it.title,
|
title = it.title,
|
||||||
description = it.description, // Support field baru
|
description = it.description,
|
||||||
content = it.content,
|
content = it.content,
|
||||||
timestamp = it.timestamp,
|
timestamp = it.timestamp,
|
||||||
isPinned = it.isPinned,
|
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>) {
|
suspend fun saveCategories(categories: List<Category>) {
|
||||||
try {
|
try {
|
||||||
context.dataStore.edit { preferences ->
|
context.dataStore.edit { preferences ->
|
||||||
@ -141,7 +164,7 @@ class DataStoreManager(private val context: Context) {
|
|||||||
id = it.id,
|
id = it.id,
|
||||||
categoryId = it.categoryId,
|
categoryId = it.categoryId,
|
||||||
title = it.title,
|
title = it.title,
|
||||||
description = it.description, // Support field baru
|
description = it.description,
|
||||||
content = it.content,
|
content = it.content,
|
||||||
timestamp = it.timestamp,
|
timestamp = it.timestamp,
|
||||||
isPinned = it.isPinned,
|
isPinned = it.isPinned,
|
||||||
@ -155,4 +178,45 @@ class DataStoreManager(private val context: Context) {
|
|||||||
e.printStackTrace()
|
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.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalClipboardManager
|
import androidx.compose.ui.platform.LocalClipboardManager
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.example.notesai.data.model.Category
|
import com.example.notesai.data.local.DataStoreManager
|
||||||
import com.example.notesai.data.model.ChatMessage
|
import com.example.notesai.data.model.*
|
||||||
import com.example.notesai.data.model.Note
|
|
||||||
import com.example.notesai.util.Constants
|
import com.example.notesai.util.Constants
|
||||||
import com.google.ai.client.generativeai.GenerativeModel
|
import com.google.ai.client.generativeai.GenerativeModel
|
||||||
import com.google.ai.client.generativeai.type.generationConfig
|
import com.google.ai.client.generativeai.type.generationConfig
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import com.example.notesai.presentation.screens.ai.components.ChatBubble
|
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.CompactStatItem
|
||||||
import com.example.notesai.presentation.screens.ai.components.SuggestionChip
|
import com.example.notesai.presentation.screens.ai.components.SuggestionChip
|
||||||
import com.example.notesai.presentation.screens.ai.components.StatItem
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@ -43,19 +41,26 @@ fun AIHelperScreen(
|
|||||||
categories: List<Category>,
|
categories: List<Category>,
|
||||||
notes: List<Note>
|
notes: List<Note>
|
||||||
) {
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val dataStoreManager = remember { DataStoreManager(context) }
|
||||||
|
|
||||||
var prompt by remember { mutableStateOf("") }
|
var prompt by remember { mutableStateOf("") }
|
||||||
var isLoading by remember { mutableStateOf(false) }
|
var isLoading by remember { mutableStateOf(false) }
|
||||||
var selectedCategory by remember { mutableStateOf<Category?>(null) }
|
var selectedCategory by remember { mutableStateOf<Category?>(null) }
|
||||||
var showCategoryDropdown by remember { mutableStateOf(false) }
|
|
||||||
var errorMessage by remember { mutableStateOf("") }
|
var errorMessage by remember { mutableStateOf("") }
|
||||||
var chatMessages by remember { mutableStateOf(listOf<ChatMessage>()) }
|
var chatMessages by remember { mutableStateOf(listOf<ChatMessage>()) }
|
||||||
var showCopiedMessage by remember { mutableStateOf(false) }
|
var showCopiedMessage by remember { mutableStateOf(false) }
|
||||||
var copiedMessageId by remember { mutableStateOf("") }
|
var copiedMessageId by remember { mutableStateOf("") }
|
||||||
|
var showHistoryDrawer by remember { mutableStateOf(false) }
|
||||||
|
var currentChatId by remember { mutableStateOf<String?>(null) }
|
||||||
|
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val clipboardManager = LocalClipboardManager.current
|
val clipboardManager = LocalClipboardManager.current
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
|
|
||||||
|
// Load chat histories
|
||||||
|
val chatHistories by dataStoreManager.chatHistoryFlow.collectAsState(initial = emptyList())
|
||||||
|
|
||||||
// Inisialisasi Gemini Model
|
// Inisialisasi Gemini Model
|
||||||
val generativeModel = remember {
|
val generativeModel = remember {
|
||||||
GenerativeModel(
|
GenerativeModel(
|
||||||
@ -79,12 +84,53 @@ fun AIHelperScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(Constants.AppColors.Background)
|
.background(Constants.AppColors.Background)
|
||||||
) {
|
) {
|
||||||
// Category Selector & Stats - Compact
|
// Top Bar with History Button & Stats
|
||||||
Surface(
|
Surface(
|
||||||
color = Constants.AppColors.Surface,
|
color = Constants.AppColors.Surface,
|
||||||
shadowElevation = 2.dp
|
shadowElevation = 2.dp
|
||||||
@ -94,69 +140,81 @@ fun AIHelperScreen(
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(16.dp)
|
.padding(16.dp)
|
||||||
) {
|
) {
|
||||||
// Category Selector
|
// Top Row - Menu & New Chat
|
||||||
Box {
|
|
||||||
Card(
|
|
||||||
onClick = { showCategoryDropdown = !showCategoryDropdown },
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
colors = CardDefaults.cardColors(
|
|
||||||
containerColor = Constants.AppColors.SurfaceVariant
|
|
||||||
),
|
|
||||||
shape = RoundedCornerShape(12.dp)
|
|
||||||
) {
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxWidth(),
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(12.dp),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
// History Drawer Button
|
||||||
|
IconButton(
|
||||||
|
onClick = { showHistoryDrawer = true },
|
||||||
|
modifier = Modifier
|
||||||
|
.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(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
modifier = Modifier.padding(
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
horizontal = Constants.Spacing.Medium.dp,
|
||||||
|
vertical = Constants.Spacing.Small.dp
|
||||||
|
),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Default.Folder,
|
Icons.Default.Folder,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = Constants.AppColors.Primary,
|
tint = Constants.AppColors.Primary,
|
||||||
modifier = Modifier.size(20.dp)
|
modifier = Modifier.size(16.dp)
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
selectedCategory?.name ?: "Semua Kategori",
|
selectedCategory?.name ?: "Semua Kategori",
|
||||||
color = Constants.AppColors.OnSurface,
|
color = Constants.AppColors.Primary,
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
fontSize = 13.sp,
|
||||||
fontWeight = FontWeight.Medium
|
fontWeight = FontWeight.SemiBold
|
||||||
)
|
|
||||||
}
|
|
||||||
Icon(
|
|
||||||
Icons.Default.ArrowDropDown,
|
|
||||||
contentDescription = null,
|
|
||||||
tint = Constants.AppColors.OnSurfaceVariant
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DropdownMenu(
|
// New Chat Button
|
||||||
expanded = showCategoryDropdown,
|
if (chatMessages.isNotEmpty()) {
|
||||||
onDismissRequest = { showCategoryDropdown = false },
|
Button(
|
||||||
modifier = Modifier
|
onClick = { startNewChat() },
|
||||||
.fillMaxWidth(0.9f)
|
colors = ButtonDefaults.buttonColors(
|
||||||
.background(Constants.AppColors.SurfaceElevated)
|
containerColor = Constants.AppColors.Primary
|
||||||
) {
|
),
|
||||||
DropdownMenuItem(
|
shape = RoundedCornerShape(Constants.Radius.Medium.dp),
|
||||||
text = { Text("Semua Kategori", color = Constants.AppColors.OnSurface) },
|
contentPadding = PaddingValues(
|
||||||
onClick = {
|
horizontal = Constants.Spacing.Medium.dp,
|
||||||
selectedCategory = null
|
vertical = Constants.Spacing.Small.dp
|
||||||
showCategoryDropdown = false
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
categories.forEach { category ->
|
) {
|
||||||
DropdownMenuItem(
|
Icon(
|
||||||
text = { Text(category.name, color = Constants.AppColors.OnSurface) },
|
Icons.Default.Add,
|
||||||
onClick = {
|
contentDescription = null,
|
||||||
selectedCategory = category
|
modifier = Modifier.size(18.dp)
|
||||||
showCategoryDropdown = false
|
)
|
||||||
}
|
Spacer(modifier = Modifier.width(6.dp))
|
||||||
|
Text(
|
||||||
|
"Baru",
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.SemiBold
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -432,6 +490,9 @@ fun AIHelperScreen(
|
|||||||
message = response,
|
message = response,
|
||||||
isUser = false
|
isUser = false
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Auto-save chat history
|
||||||
|
saveChatHistory()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
errorMessage = "Error: ${e.message}"
|
errorMessage = "Error: ${e.message}"
|
||||||
} finally {
|
} finally {
|
||||||
@ -453,4 +514,30 @@ fun AIHelperScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 {
|
object Constants {
|
||||||
// App Info
|
// App Info
|
||||||
const val APP_NAME = "NotesAI"
|
const val APP_NAME = "NotesAI"
|
||||||
const val APP_VERSION = "1.0.0"
|
const val APP_VERSION = "1.1.0"
|
||||||
|
|
||||||
// DataStore
|
// DataStore
|
||||||
const val DATASTORE_NAME = "notes_prefs"
|
const val DATASTORE_NAME = "notes_prefs"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user