Edit Title dan Filter Chat History
This commit is contained in:
parent
74e1a720cd
commit
5503d53881
@ -51,7 +51,7 @@ class DataStoreManager(private val context: Context) {
|
||||
val CATEGORIES_KEY = stringPreferencesKey("categories")
|
||||
val NOTES_KEY = stringPreferencesKey("notes")
|
||||
val CHAT_HISTORY_KEY = stringPreferencesKey("chat_history")
|
||||
val THEME_KEY = stringPreferencesKey("theme") // NEW: "dark" or "light"
|
||||
val THEME_KEY = stringPreferencesKey("theme") // "dark" or "light"
|
||||
}
|
||||
|
||||
private val json = Json {
|
||||
@ -116,7 +116,7 @@ class DataStoreManager(private val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// NEW: Chat History Flow
|
||||
// Chat History Flow
|
||||
val chatHistoryFlow: Flow<List<ChatHistory>> = context.dataStore.data
|
||||
.catch { exception ->
|
||||
if (exception is IOException) {
|
||||
@ -180,7 +180,7 @@ class DataStoreManager(private val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// NEW: Save Chat History
|
||||
// Save Chat History
|
||||
suspend fun saveChatHistory(chatHistoryList: List<ChatHistory>) {
|
||||
try {
|
||||
context.dataStore.edit { preferences ->
|
||||
@ -191,13 +191,21 @@ class DataStoreManager(private val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// NEW: Add new chat history
|
||||
// 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
|
||||
|
||||
// Check if already exists, update instead
|
||||
val existingIndex = currentList.indexOfFirst { it.id == chatHistory.id }
|
||||
if (existingIndex != -1) {
|
||||
currentList[existingIndex] = chatHistory
|
||||
} else {
|
||||
currentList.add(0, chatHistory) // Add to beginning
|
||||
}
|
||||
|
||||
preferences[CHAT_HISTORY_KEY] = json.encodeToString(currentList)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
@ -205,7 +213,7 @@ class DataStoreManager(private val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// NEW: Delete chat history (soft delete)
|
||||
// Delete chat history (soft delete)
|
||||
suspend fun deleteChatHistory(historyId: String) {
|
||||
try {
|
||||
context.dataStore.edit { preferences ->
|
||||
@ -221,7 +229,27 @@ class DataStoreManager(private val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// NEW: Theme Preference Flow
|
||||
// NEW: Update chat history title
|
||||
suspend fun updateChatHistoryTitle(historyId: String, newTitle: 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(customTitle = newTitle)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
preferences[CHAT_HISTORY_KEY] = json.encodeToString(updatedList)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
// Theme Preference Flow
|
||||
val themeFlow: Flow<String> = context.dataStore.data
|
||||
.catch { exception ->
|
||||
if (exception is IOException) {
|
||||
@ -234,7 +262,7 @@ class DataStoreManager(private val context: Context) {
|
||||
preferences[THEME_KEY] ?: "dark" // Default dark theme
|
||||
}
|
||||
|
||||
// NEW: Save Theme Preference
|
||||
// Save Theme Preference
|
||||
suspend fun saveTheme(theme: String) {
|
||||
try {
|
||||
context.dataStore.edit { preferences ->
|
||||
|
||||
@ -10,6 +10,7 @@ data class ChatHistory(
|
||||
val categoryName: String, // Untuk display
|
||||
val messages: List<SerializableChatMessage>,
|
||||
val lastMessagePreview: String, // Preview pesan terakhir
|
||||
val customTitle: String? = null, // Custom title yang di-edit user (support markdown)
|
||||
val timestamp: Long = System.currentTimeMillis(),
|
||||
val isDeleted: Boolean = false
|
||||
)
|
||||
|
||||
@ -38,6 +38,16 @@ import com.example.notesai.presentation.screens.ai.components.CompactStatItem
|
||||
import com.example.notesai.presentation.screens.ai.components.SuggestionChip
|
||||
import com.example.notesai.util.AppColors
|
||||
|
||||
private const val MAX_CHAT_TITLE_LENGTH = 30
|
||||
|
||||
private fun String.toSafeChatPreview(maxLength: Int = MAX_CHAT_TITLE_LENGTH): String {
|
||||
return if (this.length > maxLength) {
|
||||
this.take(maxLength).trim() + "..."
|
||||
} else {
|
||||
this.trim()
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AIHelperScreen(
|
||||
@ -92,9 +102,7 @@ fun AIHelperScreen(
|
||||
if (chatMessages.isNotEmpty()) {
|
||||
scope.launch {
|
||||
val lastMessage = chatMessages.lastOrNull()?.message ?: ""
|
||||
val preview = if (lastMessage.length > 100) {
|
||||
lastMessage.take(100) + "..."
|
||||
} else lastMessage
|
||||
val preview = lastMessage.toSafeChatPreview()
|
||||
|
||||
val chatHistory = ChatHistory(
|
||||
id = currentChatId ?: UUID.randomUUID().toString(),
|
||||
@ -102,6 +110,7 @@ fun AIHelperScreen(
|
||||
categoryName = selectedCategory?.name ?: "Semua Kategori",
|
||||
messages = chatMessages.map { it.toSerializable() },
|
||||
lastMessagePreview = preview,
|
||||
customTitle = null,
|
||||
timestamp = System.currentTimeMillis()
|
||||
)
|
||||
|
||||
@ -555,7 +564,12 @@ fun AIHelperScreen(
|
||||
onCategorySelected = { category ->
|
||||
selectedCategory = category
|
||||
},
|
||||
onNewChat = { startNewChat() }
|
||||
onNewChat = { startNewChat() },
|
||||
onEditHistoryTitle = { historyId, newTitle ->
|
||||
scope.launch {
|
||||
dataStoreManager.updateChatHistoryTitle(historyId, newTitle)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -499,10 +499,10 @@ private fun CompactChatHistoryItem(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(3.dp)
|
||||
) {
|
||||
// Preview Text with Markdown
|
||||
// Preview Text with Markdown (use customTitle if available)
|
||||
Text(
|
||||
text = buildAnnotatedString {
|
||||
appendInlineMarkdown(history.lastMessagePreview)
|
||||
appendInlineMarkdown(history.customTitle ?: history.lastMessagePreview)
|
||||
},
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = AppColors.OnSurface,
|
||||
@ -709,7 +709,11 @@ private fun CompactChatHistoryItem(
|
||||
|
||||
// Edit Dialog
|
||||
if (showEditDialog) {
|
||||
var editTitle by remember { mutableStateOf(history.lastMessagePreview.take(50)) }
|
||||
val maxTitleLength = 30
|
||||
var editTitle by remember {
|
||||
val currentTitle = history.customTitle ?: history.lastMessagePreview
|
||||
mutableStateOf(currentTitle.take(maxTitleLength))
|
||||
}
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = { showEditDialog = false },
|
||||
@ -730,40 +734,105 @@ private fun CompactChatHistoryItem(
|
||||
text = {
|
||||
Column {
|
||||
Text(
|
||||
"Ubah judul untuk memudahkan pencarian",
|
||||
fontSize = 13.sp,
|
||||
"Maks 30 karakter • Gunakan markdown",
|
||||
fontSize = 11.sp,
|
||||
color = AppColors.OnSurfaceVariant
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
OutlinedTextField(
|
||||
value = editTitle,
|
||||
onValueChange = { editTitle = it },
|
||||
onValueChange = {
|
||||
if (it.length <= maxTitleLength) {
|
||||
editTitle = it
|
||||
}
|
||||
},
|
||||
label = {
|
||||
Text(
|
||||
"Judul Chat",
|
||||
fontSize = 12.sp
|
||||
)
|
||||
},
|
||||
placeholder = {
|
||||
Text(
|
||||
"**Analisis** Data",
|
||||
fontSize = 11.sp
|
||||
)
|
||||
},
|
||||
supportingText = {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
Text(
|
||||
"${editTitle.length}/$maxTitleLength",
|
||||
fontSize = 10.sp,
|
||||
color = when {
|
||||
editTitle.length >= maxTitleLength -> AppColors.Error
|
||||
editTitle.length >= maxTitleLength - 5 -> AppColors.Primary
|
||||
else -> AppColors.OnSurfaceTertiary
|
||||
},
|
||||
fontWeight = if (editTitle.length >= maxTitleLength - 5)
|
||||
FontWeight.Bold
|
||||
else
|
||||
FontWeight.Normal
|
||||
)
|
||||
}
|
||||
},
|
||||
singleLine = true,
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedBorderColor = AppColors.Primary,
|
||||
focusedBorderColor = if (editTitle.length >= maxTitleLength)
|
||||
AppColors.Error
|
||||
else
|
||||
AppColors.Primary,
|
||||
unfocusedBorderColor = if (editTitle.length >= maxTitleLength)
|
||||
AppColors.Error.copy(alpha = 0.5f)
|
||||
else
|
||||
Color.Gray,
|
||||
cursorColor = AppColors.Primary
|
||||
),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
textStyle = MaterialTheme.typography.bodyMedium.copy(fontSize = 13.sp)
|
||||
)
|
||||
|
||||
// Preview
|
||||
if (editTitle.isNotBlank()) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
"Preview:",
|
||||
fontSize = 10.sp,
|
||||
color = AppColors.OnSurfaceTertiary,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Surface(
|
||||
color = AppColors.SurfaceVariant,
|
||||
shape = RoundedCornerShape(6.dp)
|
||||
) {
|
||||
Text(
|
||||
text = buildAnnotatedString {
|
||||
appendInlineMarkdown(editTitle)
|
||||
},
|
||||
modifier = Modifier.padding(8.dp),
|
||||
fontSize = 12.sp,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
Button(
|
||||
onClick = {
|
||||
if (editTitle.isNotBlank()) {
|
||||
onEdit(editTitle)
|
||||
onEdit(editTitle.trim())
|
||||
showEditDialog = false
|
||||
}
|
||||
},
|
||||
enabled = editTitle.isNotBlank() && editTitle.trim().isNotEmpty(),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = AppColors.Primary
|
||||
containerColor = AppColors.Primary,
|
||||
disabledContainerColor = AppColors.OnSurfaceVariant.copy(alpha = 0.3f)
|
||||
),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
) {
|
||||
@ -883,79 +952,80 @@ private fun groupHistoriesByDate(histories: List<ChatHistory>): Map<String, List
|
||||
|
||||
// Parse inline markdown for chat history titles
|
||||
private fun AnnotatedString.Builder.appendInlineMarkdown(text: String) {
|
||||
var currentIndex = 0
|
||||
var processedText = text
|
||||
val styles = mutableListOf<Triple<IntRange, SpanStyle, String>>()
|
||||
|
||||
// Pattern untuk bold dengan **
|
||||
val boldPattern = Regex("""\*\*(.+?)\*\*""")
|
||||
boldPattern.findAll(processedText).forEach { match ->
|
||||
styles.add(
|
||||
Triple(
|
||||
match.range,
|
||||
SpanStyle(fontWeight = FontWeight.Bold),
|
||||
match.groupValues[1]
|
||||
)
|
||||
)
|
||||
}
|
||||
// List of patterns to process
|
||||
val patterns = listOf(
|
||||
// Bold with **
|
||||
Regex("""\*\*(.+?)\*\*""") to { content: String ->
|
||||
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
|
||||
append(content)
|
||||
}
|
||||
},
|
||||
// Inline code with `
|
||||
Regex("""`(.+?)`""") to { content: String ->
|
||||
withStyle(
|
||||
SpanStyle(
|
||||
fontFamily = FontFamily.Monospace,
|
||||
background = AppColors.Primary.copy(alpha = 0.15f)
|
||||
)
|
||||
) {
|
||||
append(content)
|
||||
}
|
||||
},
|
||||
// Italic with * (but not **)
|
||||
Regex("""\*([^*]+?)\*""") to { content: String ->
|
||||
withStyle(SpanStyle(fontStyle = FontStyle.Italic)) {
|
||||
append(content)
|
||||
}
|
||||
},
|
||||
// Strikethrough with ~~
|
||||
Regex("""~~(.+?)~~""") to { content: String ->
|
||||
withStyle(SpanStyle(textDecoration = TextDecoration.LineThrough)) {
|
||||
append(content)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// Pattern untuk italic dengan *
|
||||
val italicPattern = Regex("""\*(?!\*)(.+?)\*(?!\*)""")
|
||||
italicPattern.findAll(processedText).forEach { match ->
|
||||
if (!processedText.substring(maxOf(0, match.range.first - 1), match.range.first).contains("*")) {
|
||||
styles.add(
|
||||
Triple(
|
||||
match.range,
|
||||
SpanStyle(fontStyle = FontStyle.Italic),
|
||||
match.groupValues[1]
|
||||
// Collect all matches
|
||||
data class Match(val start: Int, val end: Int, val content: String, val applier: (String) -> Unit)
|
||||
val matches = mutableListOf<Match>()
|
||||
|
||||
patterns.forEach { (regex, applier) ->
|
||||
regex.findAll(processedText).forEach { matchResult ->
|
||||
val content = matchResult.groupValues[1]
|
||||
matches.add(
|
||||
Match(
|
||||
start = matchResult.range.first,
|
||||
end = matchResult.range.last + 1,
|
||||
content = content,
|
||||
applier = applier
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Pattern untuk inline code dengan `
|
||||
val codePattern = Regex("""`(.+?)`""")
|
||||
codePattern.findAll(processedText).forEach { match ->
|
||||
styles.add(
|
||||
Triple(
|
||||
match.range,
|
||||
SpanStyle(
|
||||
fontFamily = FontFamily.Monospace,
|
||||
background = AppColors.Primary.copy(alpha = 0.15f),
|
||||
fontSize = 12.sp
|
||||
),
|
||||
match.groupValues[1]
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// Pattern untuk strikethrough dengan ~~
|
||||
val strikePattern = Regex("""~~(.+?)~~""")
|
||||
strikePattern.findAll(processedText).forEach { match ->
|
||||
styles.add(
|
||||
Triple(
|
||||
match.range,
|
||||
SpanStyle(textDecoration = TextDecoration.LineThrough),
|
||||
match.groupValues[1]
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// Sort by range start
|
||||
val sortedStyles = styles.sortedBy { it.first.first }
|
||||
// Sort matches by start position and remove overlaps
|
||||
val sortedMatches = matches
|
||||
.sortedBy { it.start }
|
||||
.fold(mutableListOf<Match>()) { acc, match ->
|
||||
if (acc.isEmpty() || match.start >= acc.last().end) {
|
||||
acc.add(match)
|
||||
}
|
||||
acc
|
||||
}
|
||||
|
||||
// Build the annotated string
|
||||
var lastIndex = 0
|
||||
sortedStyles.forEach { (range, style, content) ->
|
||||
// Append text before styled content
|
||||
if (range.first > lastIndex) {
|
||||
append(processedText.substring(lastIndex, range.first))
|
||||
sortedMatches.forEach { match ->
|
||||
// Append text before match
|
||||
if (match.start > lastIndex) {
|
||||
append(processedText.substring(lastIndex, match.start))
|
||||
}
|
||||
|
||||
// Append styled content
|
||||
withStyle(style) {
|
||||
append(content)
|
||||
}
|
||||
|
||||
lastIndex = range.last + 1
|
||||
// Apply style
|
||||
match.applier(match.content)
|
||||
lastIndex = match.end
|
||||
}
|
||||
|
||||
// Append remaining text
|
||||
Loading…
x
Reference in New Issue
Block a user