diff --git a/Readme.md b/Readme.md index 43b064a..e84154d 100644 --- a/Readme.md +++ b/Readme.md @@ -199,7 +199,7 @@ * Sistem kategori dengan gradient * Buat/edit/hapus kategori dengan confirmation dialog * Buat/edit/hapus catatan -* Pin catatan penting +* Pin catatan penting (Catatan Berbintang) * Full-screen editor * Search kategori di beranda * Search catatan dalam kategori @@ -208,7 +208,7 @@ * AI membaca & menganalisis catatan pengguna * Suggestion chips & copy response * Filter AI berdasarkan kategori -* Dark theme modern + gradient +* Dark theme & Light theme * Animasi smooth * Empty states & error handling @@ -226,7 +226,6 @@ ## **Features for Sprint 4 v1.1.0** * Penyesuaian UI/UX History Chat AI (ok) -* Rich text editor (ok - Pengembangan Lanjutan) -* AI Agent Catatan +* Rich text editor (ok - Harus Pengembangan Lanjutan) * Fungsi AI (Upload File) * Fitur Sematkan Category, otomatis paling atas diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 484bc63..47f6b97 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -80,4 +80,10 @@ dependencies { androidTestImplementation("androidx.compose.ui:ui-test-junit4") debugImplementation("androidx.compose.ui:ui-tooling") debugImplementation("androidx.compose.ui:ui-test-manifest") + + // Gemini AI + implementation("com.google.ai.client.generativeai:generativeai:0.1.2") + + // JSON parsing (untuk tool calls) + implementation("org.json:json:20230227") } \ No newline at end of file diff --git a/app/src/main/java/com/example/notesai/MainActivity.kt b/app/src/main/java/com/example/notesai/MainActivity.kt index a733bb8..3b486fa 100644 --- a/app/src/main/java/com/example/notesai/MainActivity.kt +++ b/app/src/main/java/com/example/notesai/MainActivity.kt @@ -97,7 +97,6 @@ fun NotesApp() { val dataStoreManager = remember { DataStoreManager(context) } val scope = rememberCoroutineScope() val lifecycleOwner = androidx.lifecycle.compose.LocalLifecycleOwner.current - var categories by remember { mutableStateOf(listOf()) } var notes by remember { mutableStateOf(listOf()) } var selectedCategory by remember { mutableStateOf(null) } diff --git a/app/src/main/java/com/example/notesai/presentation/screens/ai/AIHelperScreen.kt b/app/src/main/java/com/example/notesai/presentation/screens/ai/AIHelperScreen.kt index b95f2bc..1cb08f4 100644 --- a/app/src/main/java/com/example/notesai/presentation/screens/ai/AIHelperScreen.kt +++ b/app/src/main/java/com/example/notesai/presentation/screens/ai/AIHelperScreen.kt @@ -345,8 +345,8 @@ fun AIHelperScreen( chatMessages.forEach { message -> ChatBubble( message = message, - onCopy = { - clipboardManager.setText(AnnotatedString(message.message)) + onCopy = { textToCopy -> // CHANGED: Sekarang terima parameter text + clipboardManager.setText(AnnotatedString(textToCopy)) copiedMessageId = message.id showCopiedMessage = true scope.launch { diff --git a/app/src/main/java/com/example/notesai/presentation/screens/ai/components/ChatBubble.kt b/app/src/main/java/com/example/notesai/presentation/screens/ai/components/ChatBubble.kt index 7ac8b5f..49f3d89 100644 --- a/app/src/main/java/com/example/notesai/presentation/screens/ai/components/ChatBubble.kt +++ b/app/src/main/java/com/example/notesai/presentation/screens/ai/components/ChatBubble.kt @@ -5,10 +5,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Check -import androidx.compose.material.icons.filled.ContentCopy -import androidx.compose.material.icons.filled.Person -import androidx.compose.material.icons.filled.SmartToy +import androidx.compose.material.icons.filled.* import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment @@ -19,6 +16,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.example.notesai.data.model.ChatMessage import com.example.notesai.util.MarkdownText +import com.example.notesai.util.MarkdownStripper import com.example.notesai.util.AppColors import com.example.notesai.util.Constants import java.text.SimpleDateFormat @@ -27,17 +25,18 @@ import java.util.* @Composable fun ChatBubble( message: ChatMessage, - onCopy: () -> Unit, + onCopy: (String) -> Unit, // CHANGED: Sekarang terima text parameter showCopied: Boolean ) { val dateFormat = remember { SimpleDateFormat("HH:mm", Locale("id", "ID")) } + var showCopyMenu by remember { mutableStateOf(false) } Column( modifier = Modifier.fillMaxWidth(), horizontalAlignment = if (message.isUser) Alignment.End else Alignment.Start ) { if (message.isUser) { - // User Message + // User Message (tidak berubah) Surface( color = AppColors.Primary, shape = RoundedCornerShape( @@ -89,7 +88,7 @@ fun ChatBubble( } } } else { - // AI Message with Markdown + // AI Message with IMPROVED Copy Options Surface( color = AppColors.SurfaceVariant, shape = RoundedCornerShape( @@ -127,23 +126,103 @@ fun ChatBubble( ) } - // Copy Button - IconButton( - onClick = onCopy, - modifier = Modifier.size(28.dp) - ) { - AnimatedContent( - targetState = showCopied, - transitionSpec = { - fadeIn() + scaleIn() togetherWith fadeOut() + scaleOut() - }, - label = "copy_icon" - ) { copied -> - Icon( - if (copied) Icons.Default.Check else Icons.Default.ContentCopy, - contentDescription = if (copied) "Copied" else "Copy", - tint = if (copied) AppColors.Success else AppColors.OnSurfaceVariant, - modifier = Modifier.size(16.dp) + // IMPROVED: Copy Button with Dropdown Menu + Box { + IconButton( + onClick = { showCopyMenu = !showCopyMenu }, + modifier = Modifier.size(28.dp) + ) { + AnimatedContent( + targetState = showCopied, + transitionSpec = { + fadeIn() + scaleIn() togetherWith fadeOut() + scaleOut() + }, + label = "copy_icon" + ) { copied -> + Icon( + if (copied) Icons.Default.Check else Icons.Default.ContentCopy, + contentDescription = if (copied) "Copied" else "Copy", + tint = if (copied) AppColors.Success else AppColors.OnSurfaceVariant, + modifier = Modifier.size(16.dp) + ) + } + } + + // Dropdown Menu untuk pilihan copy + DropdownMenu( + expanded = showCopyMenu, + onDismissRequest = { showCopyMenu = false }, + modifier = Modifier.background(AppColors.SurfaceElevated) + ) { + // Option 1: Copy dengan Format (Markdown) + DropdownMenuItem( + text = { + Row( + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + Icons.Default.Code, + contentDescription = null, + tint = AppColors.Primary, + modifier = Modifier.size(18.dp) + ) + Column { + Text( + "Copy dengan Format", + fontSize = 13.sp, + color = AppColors.OnSurface, + fontWeight = FontWeight.Medium + ) + Text( + "Termasuk markdown", + fontSize = 10.sp, + color = AppColors.OnSurfaceTertiary + ) + } + } + }, + onClick = { + onCopy(message.message) // Copy original dengan markdown + showCopyMenu = false + } + ) + + HorizontalDivider(color = AppColors.Divider) + + // Option 2: Copy Teks Asli (Plain Text) + DropdownMenuItem( + text = { + Row( + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + Icons.Default.TextFields, + contentDescription = null, + tint = AppColors.Secondary, + modifier = Modifier.size(18.dp) + ) + Column { + Text( + "Copy Teks Asli", + fontSize = 13.sp, + color = AppColors.OnSurface, + fontWeight = FontWeight.Medium + ) + Text( + "Tanpa format", + fontSize = 10.sp, + color = AppColors.OnSurfaceTertiary + ) + } + } + }, + onClick = { + val plainText = MarkdownStripper.stripMarkdown(message.message) + onCopy(plainText) // Copy plain text tanpa markdown + showCopyMenu = false + } ) } } diff --git a/app/src/main/java/com/example/notesai/presentation/screens/note/EditableFullScreenNoteView.kt b/app/src/main/java/com/example/notesai/presentation/screens/note/EditableFullScreenNoteView.kt index 6b8daf8..298dacc 100644 --- a/app/src/main/java/com/example/notesai/presentation/screens/note/EditableFullScreenNoteView.kt +++ b/app/src/main/java/com/example/notesai/presentation/screens/note/EditableFullScreenNoteView.kt @@ -29,8 +29,6 @@ import androidx.compose.ui.unit.* import com.example.notesai.data.model.Note import com.example.notesai.presentation.screens.note.components.DraggableMiniMarkdownToolbar import com.example.notesai.presentation.screens.note.editor.RichEditorState -import com.example.notesai.util.MarkdownParser -import com.example.notesai.util.MarkdownSerializer import kotlinx.coroutines.launch import java.text.SimpleDateFormat import java.util.* diff --git a/app/src/main/java/com/example/notesai/util/MarkdownParser.kt b/app/src/main/java/com/example/notesai/util/MarkdownParser.kt deleted file mode 100644 index e9901e0..0000000 --- a/app/src/main/java/com/example/notesai/util/MarkdownParser.kt +++ /dev/null @@ -1,58 +0,0 @@ -package com.example.notesai.util - -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.font.FontStyle -import androidx.compose.ui.text.font.FontWeight - -object MarkdownParser { - - fun parse(markdown: String): AnnotatedString { - val builder = AnnotatedString.Builder() - var i = 0 - - while (i < markdown.length) { - when { - markdown.startsWith("**", i) -> { - val end = markdown.indexOf("**", i + 2) - if (end != -1) { - val content = markdown.substring(i + 2, end) - val start = builder.length - builder.append(content) - builder.addStyle( - SpanStyle(fontWeight = FontWeight.Bold), - start, - start + content.length - ) - i = end + 2 - } else { - builder.append(markdown[i++]) - } - } - - markdown.startsWith("*", i) -> { - val end = markdown.indexOf("*", i + 1) - if (end != -1) { - val content = markdown.substring(i + 1, end) - val start = builder.length - builder.append(content) - builder.addStyle( - SpanStyle(fontStyle = FontStyle.Italic), - start, - start + content.length - ) - i = end + 1 - } else { - builder.append(markdown[i++]) - } - } - - else -> { - builder.append(markdown[i++]) - } - } - } - - return builder.toAnnotatedString() - } -} diff --git a/app/src/main/java/com/example/notesai/util/MarkdownSerializer.kt b/app/src/main/java/com/example/notesai/util/MarkdownSerializer.kt deleted file mode 100644 index 25e38ca..0000000 --- a/app/src/main/java/com/example/notesai/util/MarkdownSerializer.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.example.notesai.util - -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.font.FontStyle -import androidx.compose.ui.text.font.FontWeight - -object MarkdownSerializer { - - fun toMarkdown(text: AnnotatedString): String { - val raw = text.text - if (text.spanStyles.isEmpty()) return raw - - val markers = Array(raw.length + 1) { mutableListOf() } - - text.spanStyles.forEach { span -> - if (span.item.fontWeight == FontWeight.Bold) { - markers[span.start].add("**") - markers[span.end].add("**") - } - if (span.item.fontStyle == FontStyle.Italic) { - markers[span.start].add("*") - markers[span.end].add("*") - } - } - - val sb = StringBuilder() - for (i in raw.indices) { - markers[i].forEach { sb.append(it) } - sb.append(raw[i]) - } - markers[raw.length].forEach { sb.append(it) } - - return sb.toString() - } -} diff --git a/app/src/main/java/com/example/notesai/util/MarkdownStripper.kt b/app/src/main/java/com/example/notesai/util/MarkdownStripper.kt new file mode 100644 index 0000000..3b96355 --- /dev/null +++ b/app/src/main/java/com/example/notesai/util/MarkdownStripper.kt @@ -0,0 +1,68 @@ +package com.example.notesai.util + +/** + * Utility untuk convert markdown text ke plain text + */ +object MarkdownStripper { + + /** + * Strip semua markdown formatting dan return plain text + */ + fun stripMarkdown(text: String): String { + var result = text + + // 1. Remove code blocks (```...```) + result = result.replace(Regex("""```[\s\S]*?```"""), "") + + // 2. Remove inline code (`...`) + result = result.replace(Regex("""`([^`]+)`"""), "$1") + + // 3. Remove bold (**...**) + result = result.replace(Regex("""\*\*([^*]+)\*\*"""), "$1") + + // 4. Remove italic (*...*) + result = result.replace(Regex("""\*([^*]+)\*"""), "$1") + + // 5. Remove strikethrough (~~...~~) + result = result.replace(Regex("""~~([^~]+)~~"""), "$1") + + // 6. Remove headers (# ## ### etc) + result = result.replace(Regex("""^#{1,6}\s+""", RegexOption.MULTILINE), "") + + // 7. Remove links [text](url) → text + result = result.replace(Regex("""\[([^\]]+)\]\([^)]+\)"""), "$1") + + // 8. Remove images ![alt](url) → alt + result = result.replace(Regex("""!\[([^\]]*)\]\([^)]+\)"""), "$1") + + // 9. Remove horizontal rules (---, ***, ___) + result = result.replace(Regex("""^[-*_]{3,}$""", RegexOption.MULTILINE), "") + + // 10. Remove blockquotes (> ...) + result = result.replace(Regex("""^>\s+""", RegexOption.MULTILINE), "") + + // 11. Remove unordered list markers (-, *, +) + result = result.replace(Regex("""^[\s]*[-*+]\s+""", RegexOption.MULTILINE), "") + + // 12. Remove ordered list markers (1. 2. 3.) + result = result.replace(Regex("""^[\s]*\d+\.\s+""", RegexOption.MULTILINE), "") + + // 13. Clean up extra whitespace + result = result.replace(Regex("""\n{3,}"""), "\n\n") // Max 2 consecutive newlines + result = result.trim() + + return result + } + + /** + * Get preview text (first N characters, stripped) + */ + fun getPlainPreview(text: String, maxLength: Int = 100): String { + val plain = stripMarkdown(text) + return if (plain.length > maxLength) { + plain.take(maxLength).trim() + "..." + } else { + plain + } + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml deleted file mode 100644 index 86a5d97..0000000 --- a/app/src/main/res/layout/activity_main.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml index 3b9472b..69a9dc4 100644 --- a/app/src/main/res/values-night/themes.xml +++ b/app/src/main/res/values-night/themes.xml @@ -1,7 +1,3 @@ - \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index c8524cd..a6b3dae 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -1,5 +1,2 @@ - - #FF000000 - #FFFFFFFF - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 2350d5d..7d5c507 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -1,10 +1,5 @@ -