Plain Text Copy
This commit is contained in:
parent
f4847ced63
commit
978b4285bb
@ -199,7 +199,7 @@
|
|||||||
* Sistem kategori dengan gradient
|
* Sistem kategori dengan gradient
|
||||||
* Buat/edit/hapus kategori dengan confirmation dialog
|
* Buat/edit/hapus kategori dengan confirmation dialog
|
||||||
* Buat/edit/hapus catatan
|
* Buat/edit/hapus catatan
|
||||||
* Pin catatan penting
|
* Pin catatan penting (Catatan Berbintang)
|
||||||
* Full-screen editor
|
* Full-screen editor
|
||||||
* Search kategori di beranda
|
* Search kategori di beranda
|
||||||
* Search catatan dalam kategori
|
* Search catatan dalam kategori
|
||||||
@ -208,7 +208,7 @@
|
|||||||
* AI membaca & menganalisis catatan pengguna
|
* AI membaca & menganalisis catatan pengguna
|
||||||
* Suggestion chips & copy response
|
* Suggestion chips & copy response
|
||||||
* Filter AI berdasarkan kategori
|
* Filter AI berdasarkan kategori
|
||||||
* Dark theme modern + gradient
|
* Dark theme & Light theme
|
||||||
* Animasi smooth
|
* Animasi smooth
|
||||||
* Empty states & error handling
|
* Empty states & error handling
|
||||||
|
|
||||||
@ -226,7 +226,6 @@
|
|||||||
## **Features for Sprint 4 v1.1.0**
|
## **Features for Sprint 4 v1.1.0**
|
||||||
|
|
||||||
* Penyesuaian UI/UX History Chat AI (ok)
|
* Penyesuaian UI/UX History Chat AI (ok)
|
||||||
* Rich text editor (ok - Pengembangan Lanjutan)
|
* Rich text editor (ok - Harus Pengembangan Lanjutan)
|
||||||
* AI Agent Catatan
|
|
||||||
* Fungsi AI (Upload File)
|
* Fungsi AI (Upload File)
|
||||||
* Fitur Sematkan Category, otomatis paling atas
|
* Fitur Sematkan Category, otomatis paling atas
|
||||||
|
|||||||
@ -80,4 +80,10 @@ dependencies {
|
|||||||
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
|
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
|
||||||
debugImplementation("androidx.compose.ui:ui-tooling")
|
debugImplementation("androidx.compose.ui:ui-tooling")
|
||||||
debugImplementation("androidx.compose.ui:ui-test-manifest")
|
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")
|
||||||
}
|
}
|
||||||
@ -97,7 +97,6 @@ fun NotesApp() {
|
|||||||
val dataStoreManager = remember { DataStoreManager(context) }
|
val dataStoreManager = remember { DataStoreManager(context) }
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val lifecycleOwner = androidx.lifecycle.compose.LocalLifecycleOwner.current
|
val lifecycleOwner = androidx.lifecycle.compose.LocalLifecycleOwner.current
|
||||||
|
|
||||||
var categories by remember { mutableStateOf(listOf<Category>()) }
|
var categories by remember { mutableStateOf(listOf<Category>()) }
|
||||||
var notes by remember { mutableStateOf(listOf<Note>()) }
|
var notes by remember { mutableStateOf(listOf<Note>()) }
|
||||||
var selectedCategory by remember { mutableStateOf<Category?>(null) }
|
var selectedCategory by remember { mutableStateOf<Category?>(null) }
|
||||||
|
|||||||
@ -345,8 +345,8 @@ fun AIHelperScreen(
|
|||||||
chatMessages.forEach { message ->
|
chatMessages.forEach { message ->
|
||||||
ChatBubble(
|
ChatBubble(
|
||||||
message = message,
|
message = message,
|
||||||
onCopy = {
|
onCopy = { textToCopy -> // CHANGED: Sekarang terima parameter text
|
||||||
clipboardManager.setText(AnnotatedString(message.message))
|
clipboardManager.setText(AnnotatedString(textToCopy))
|
||||||
copiedMessageId = message.id
|
copiedMessageId = message.id
|
||||||
showCopiedMessage = true
|
showCopiedMessage = true
|
||||||
scope.launch {
|
scope.launch {
|
||||||
|
|||||||
@ -5,10 +5,7 @@ import androidx.compose.foundation.background
|
|||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Check
|
import androidx.compose.material.icons.filled.*
|
||||||
import androidx.compose.material.icons.filled.ContentCopy
|
|
||||||
import androidx.compose.material.icons.filled.Person
|
|
||||||
import androidx.compose.material.icons.filled.SmartToy
|
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
@ -19,6 +16,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.example.notesai.data.model.ChatMessage
|
import com.example.notesai.data.model.ChatMessage
|
||||||
import com.example.notesai.util.MarkdownText
|
import com.example.notesai.util.MarkdownText
|
||||||
|
import com.example.notesai.util.MarkdownStripper
|
||||||
import com.example.notesai.util.AppColors
|
import com.example.notesai.util.AppColors
|
||||||
import com.example.notesai.util.Constants
|
import com.example.notesai.util.Constants
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
@ -27,17 +25,18 @@ import java.util.*
|
|||||||
@Composable
|
@Composable
|
||||||
fun ChatBubble(
|
fun ChatBubble(
|
||||||
message: ChatMessage,
|
message: ChatMessage,
|
||||||
onCopy: () -> Unit,
|
onCopy: (String) -> Unit, // CHANGED: Sekarang terima text parameter
|
||||||
showCopied: Boolean
|
showCopied: Boolean
|
||||||
) {
|
) {
|
||||||
val dateFormat = remember { SimpleDateFormat("HH:mm", Locale("id", "ID")) }
|
val dateFormat = remember { SimpleDateFormat("HH:mm", Locale("id", "ID")) }
|
||||||
|
var showCopyMenu by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalAlignment = if (message.isUser) Alignment.End else Alignment.Start
|
horizontalAlignment = if (message.isUser) Alignment.End else Alignment.Start
|
||||||
) {
|
) {
|
||||||
if (message.isUser) {
|
if (message.isUser) {
|
||||||
// User Message
|
// User Message (tidak berubah)
|
||||||
Surface(
|
Surface(
|
||||||
color = AppColors.Primary,
|
color = AppColors.Primary,
|
||||||
shape = RoundedCornerShape(
|
shape = RoundedCornerShape(
|
||||||
@ -89,7 +88,7 @@ fun ChatBubble(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// AI Message with Markdown
|
// AI Message with IMPROVED Copy Options
|
||||||
Surface(
|
Surface(
|
||||||
color = AppColors.SurfaceVariant,
|
color = AppColors.SurfaceVariant,
|
||||||
shape = RoundedCornerShape(
|
shape = RoundedCornerShape(
|
||||||
@ -127,9 +126,10 @@ fun ChatBubble(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy Button
|
// IMPROVED: Copy Button with Dropdown Menu
|
||||||
|
Box {
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = onCopy,
|
onClick = { showCopyMenu = !showCopyMenu },
|
||||||
modifier = Modifier.size(28.dp)
|
modifier = Modifier.size(28.dp)
|
||||||
) {
|
) {
|
||||||
AnimatedContent(
|
AnimatedContent(
|
||||||
@ -147,6 +147,85 @@ fun ChatBubble(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|||||||
@ -29,8 +29,6 @@ import androidx.compose.ui.unit.*
|
|||||||
import com.example.notesai.data.model.Note
|
import com.example.notesai.data.model.Note
|
||||||
import com.example.notesai.presentation.screens.note.components.DraggableMiniMarkdownToolbar
|
import com.example.notesai.presentation.screens.note.components.DraggableMiniMarkdownToolbar
|
||||||
import com.example.notesai.presentation.screens.note.editor.RichEditorState
|
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 kotlinx.coroutines.launch
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|||||||
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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<String>() }
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,19 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:id="@+id/main"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
tools:context=".MainActivity">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="Hello World!"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
@ -1,7 +1,3 @@
|
|||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
<!-- Base application theme. -->
|
<!-- Base application theme. -->
|
||||||
<style name="Base.Theme.Notesai" parent="Theme.Material3.DayNight.NoActionBar">
|
|
||||||
<!-- Customize your dark theme here. -->
|
|
||||||
<!-- <item name="colorPrimary">@color/my_dark_primary</item> -->
|
|
||||||
</style>
|
|
||||||
</resources>
|
</resources>
|
||||||
@ -1,5 +1,2 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources></resources>
|
||||||
<color name="black">#FF000000</color>
|
|
||||||
<color name="white">#FFFFFFFF</color>
|
|
||||||
</resources>
|
|
||||||
@ -1,10 +1,5 @@
|
|||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
<style name="Theme.NotesAI" parent="android:Theme.Material.Light.NoActionBar" />
|
<style name="Theme.NotesAI" parent="android:Theme.Material.Light.NoActionBar" />
|
||||||
<!-- Base application theme. -->
|
<!-- Base application theme. -->
|
||||||
<style name="Base.Theme.Notesai" parent="Theme.Material3.DayNight.NoActionBar">
|
|
||||||
<!-- Customize your light theme here. -->
|
|
||||||
<!-- <item name="colorPrimary">@color/my_light_primary</item> -->
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style name="Theme.Notesai" parent="Base.Theme.Notesai" />
|
|
||||||
</resources>
|
</resources>
|
||||||
Loading…
x
Reference in New Issue
Block a user