fix: revert SpeechQueue TTS batching, keep TTS text cleaning
Reverted the sentence-batching SpeechQueue that caused duplicate playback. TTS now uses the original simple approach: speak full text once on Final. Retained: parenthetical content stripping + emoji removal for cleaner TTS output. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -51,6 +51,7 @@ data class AgentResponse(
|
|||||||
val id: String,
|
val id: String,
|
||||||
val name: String,
|
val name: String,
|
||||||
val description: String?,
|
val description: String?,
|
||||||
|
val avatar: String? = null,
|
||||||
@SerializedName("workflow_config") val workflowConfig: Map<String, Any?>?,
|
@SerializedName("workflow_config") val workflowConfig: Map<String, Any?>?,
|
||||||
@SerializedName("budget_config") val budgetConfig: Map<String, Any?>?,
|
@SerializedName("budget_config") val budgetConfig: Map<String, Any?>?,
|
||||||
val version: Int = 1,
|
val version: Int = 1,
|
||||||
@@ -268,7 +269,8 @@ data class AsrResponse(
|
|||||||
|
|
||||||
data class TtsRequest(
|
data class TtsRequest(
|
||||||
val text: String,
|
val text: String,
|
||||||
val voice: String = "alloy"
|
val voice: String = "xiaoxiao",
|
||||||
|
val speed: Float = 1.0f
|
||||||
)
|
)
|
||||||
|
|
||||||
data class TtsResponse(
|
data class TtsResponse(
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ class AgentRepository @Inject constructor(
|
|||||||
id = dto.id,
|
id = dto.id,
|
||||||
name = dto.name ?: "",
|
name = dto.name ?: "",
|
||||||
description = dto.description ?: "",
|
description = dto.description ?: "",
|
||||||
|
avatar = dto.avatar,
|
||||||
status = dto.status ?: "published",
|
status = dto.status ?: "published",
|
||||||
userId = dto.userId,
|
userId = dto.userId,
|
||||||
version = dto.version,
|
version = dto.version,
|
||||||
@@ -50,6 +51,7 @@ class AgentRepository @Inject constructor(
|
|||||||
id = dto.id,
|
id = dto.id,
|
||||||
name = dto.name ?: "",
|
name = dto.name ?: "",
|
||||||
description = dto.description ?: "",
|
description = dto.description ?: "",
|
||||||
|
avatar = dto.avatar,
|
||||||
status = dto.status ?: "published",
|
status = dto.status ?: "published",
|
||||||
userId = dto.userId,
|
userId = dto.userId,
|
||||||
version = dto.version,
|
version = dto.version,
|
||||||
@@ -150,6 +152,7 @@ class AgentRepository @Inject constructor(
|
|||||||
id = id,
|
id = id,
|
||||||
name = name ?: "",
|
name = name ?: "",
|
||||||
description = description ?: "",
|
description = description ?: "",
|
||||||
|
avatar = avatar,
|
||||||
status = status ?: "published",
|
status = status ?: "published",
|
||||||
userId = userId,
|
userId = userId,
|
||||||
version = version,
|
version = version,
|
||||||
|
|||||||
@@ -8,5 +8,6 @@ data class Agent(
|
|||||||
val userId: String? = null,
|
val userId: String? = null,
|
||||||
val version: Int = 1,
|
val version: Int = 1,
|
||||||
val createdAt: String = "",
|
val createdAt: String = "",
|
||||||
val updatedAt: String = ""
|
val updatedAt: String = "",
|
||||||
|
val avatar: String? = null // emoji or image URL
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.tiangong.aiagent.ui.chat
|
|||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.PickVisualMediaRequest
|
import androidx.activity.result.PickVisualMediaRequest
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
@@ -74,6 +75,11 @@ fun ChatScreen(
|
|||||||
// Drawer state
|
// Drawer state
|
||||||
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
|
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
|
||||||
|
|
||||||
|
// Close drawer on system back press
|
||||||
|
BackHandler(enabled = drawerState.isOpen) {
|
||||||
|
scope.launch { drawerState.close() }
|
||||||
|
}
|
||||||
|
|
||||||
// Track first load for skeleton
|
// Track first load for skeleton
|
||||||
LaunchedEffect(uiState.messages.size, uiState.isStreaming) {
|
LaunchedEffect(uiState.messages.size, uiState.isStreaming) {
|
||||||
if (uiState.messages.isNotEmpty() || uiState.isStreaming || uiState.currentAgent != null) {
|
if (uiState.messages.isNotEmpty() || uiState.isStreaming || uiState.currentAgent != null) {
|
||||||
@@ -663,7 +669,11 @@ fun ChatScreen(
|
|||||||
if (showFeedback) {
|
if (showFeedback) {
|
||||||
val clipboardManager = LocalClipboardManager.current
|
val clipboardManager = LocalClipboardManager.current
|
||||||
Column {
|
Column {
|
||||||
MessageBubbleFromUi(message = message)
|
MessageBubbleFromUi(
|
||||||
|
message = message,
|
||||||
|
agentName = uiState.currentAgent?.name ?: "助手",
|
||||||
|
agentAvatar = uiState.currentAgent?.avatar
|
||||||
|
)
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.padding(start = 56.dp, bottom = 4.dp),
|
modifier = Modifier.padding(start = 56.dp, bottom = 4.dp),
|
||||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
@@ -794,7 +804,11 @@ fun ChatScreen(
|
|||||||
// User message: show bubble with copy & share actions
|
// User message: show bubble with copy & share actions
|
||||||
val clipboardManager = LocalClipboardManager.current
|
val clipboardManager = LocalClipboardManager.current
|
||||||
Column {
|
Column {
|
||||||
MessageBubbleFromUi(message = message)
|
MessageBubbleFromUi(
|
||||||
|
message = message,
|
||||||
|
agentName = uiState.currentAgent?.name ?: "助手",
|
||||||
|
agentAvatar = uiState.currentAgent?.avatar
|
||||||
|
)
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.padding(end = 16.dp, bottom = 4.dp),
|
modifier = Modifier.padding(end = 16.dp, bottom = 4.dp),
|
||||||
horizontalArrangement = Arrangement.End,
|
horizontalArrangement = Arrangement.End,
|
||||||
@@ -904,8 +918,16 @@ fun ChatScreen(
|
|||||||
|
|
||||||
/** Thin adapter to render UiMessage via existing MessageBubble. */
|
/** Thin adapter to render UiMessage via existing MessageBubble. */
|
||||||
@Composable
|
@Composable
|
||||||
private fun MessageBubbleFromUi(message: UiMessage) {
|
private fun MessageBubbleFromUi(
|
||||||
com.tiangong.aiagent.ui.chat.components.MessageBubble(message = message.toDomainMessage())
|
message: UiMessage,
|
||||||
|
agentName: String = "助手",
|
||||||
|
agentAvatar: String? = null
|
||||||
|
) {
|
||||||
|
com.tiangong.aiagent.ui.chat.components.MessageBubble(
|
||||||
|
message = message.toDomainMessage(),
|
||||||
|
agentName = agentName,
|
||||||
|
agentAvatar = agentAvatar
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun UiMessage.toDomainMessage(): com.tiangong.aiagent.domain.model.Message {
|
private fun UiMessage.toDomainMessage(): com.tiangong.aiagent.domain.model.Message {
|
||||||
|
|||||||
@@ -437,8 +437,12 @@ class ChatViewModel @Inject constructor(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Capture agent context at call time to avoid any race condition
|
// Guard: don't send without an agent (prevents routing to non-existent bare SSE endpoint)
|
||||||
val agentId = _uiState.value.currentAgent?.id
|
val agentId = _uiState.value.currentAgent?.id
|
||||||
|
if (agentId == null) {
|
||||||
|
_uiState.value = _uiState.value.copy(error = "请先选择一个智能体")
|
||||||
|
return
|
||||||
|
}
|
||||||
val sessionId = _uiState.value.sessionId ?: UUID.randomUUID().toString()
|
val sessionId = _uiState.value.sessionId ?: UUID.randomUUID().toString()
|
||||||
|
|
||||||
// Analytics: track message send
|
// Analytics: track message send
|
||||||
@@ -999,6 +1003,7 @@ class ChatViewModel @Inject constructor(
|
|||||||
super.onCleared()
|
super.onCleared()
|
||||||
sseJob?.cancel()
|
sseJob?.cancel()
|
||||||
historyFlowJob?.cancel()
|
historyFlowJob?.cancel()
|
||||||
|
audioPlayer.release()
|
||||||
audioRecorder.cleanup()
|
audioRecorder.cleanup()
|
||||||
persistSession()
|
persistSession()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,12 @@ import androidx.compose.material3.*
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import coil.compose.AsyncImage
|
||||||
|
import coil.request.ImageRequest
|
||||||
import com.tiangong.aiagent.domain.model.Message
|
import com.tiangong.aiagent.domain.model.Message
|
||||||
import com.tiangong.aiagent.util.MarkdownRenderer
|
import com.tiangong.aiagent.util.MarkdownRenderer
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
@@ -18,7 +22,11 @@ import java.util.Date
|
|||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MessageBubble(message: Message) {
|
fun MessageBubble(
|
||||||
|
message: Message,
|
||||||
|
agentName: String = "助手",
|
||||||
|
agentAvatar: String? = null
|
||||||
|
) {
|
||||||
val isUser = message.role == Message.Role.USER
|
val isUser = message.role == Message.Role.USER
|
||||||
val isTool = message.role == Message.Role.TOOL
|
val isTool = message.role == Message.Role.TOOL
|
||||||
val alignment = if (isUser) Alignment.End else Alignment.Start
|
val alignment = if (isUser) Alignment.End else Alignment.Start
|
||||||
@@ -42,15 +50,11 @@ fun MessageBubble(message: Message) {
|
|||||||
) {
|
) {
|
||||||
if (!isUser && !isTool) {
|
if (!isUser && !isTool) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Icon(
|
// Avatar: emoji, image URL, or default icon
|
||||||
imageVector = Icons.Default.SmartToy,
|
AgentAvatar(agentAvatar = agentAvatar)
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.size(16.dp),
|
|
||||||
tint = MaterialTheme.colorScheme.primary
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
Text(
|
Text(
|
||||||
text = "助手",
|
text = agentName,
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
color = MaterialTheme.colorScheme.primary,
|
color = MaterialTheme.colorScheme.primary,
|
||||||
fontWeight = FontWeight.Bold
|
fontWeight = FontWeight.Bold
|
||||||
@@ -111,3 +115,40 @@ private fun formatTimestamp(epochMillis: Long): String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AgentAvatar(agentAvatar: String?) {
|
||||||
|
when {
|
||||||
|
// Image URL
|
||||||
|
agentAvatar != null && (agentAvatar.startsWith("http") ||
|
||||||
|
agentAvatar.startsWith("/") ||
|
||||||
|
agentAvatar.startsWith("data:")) -> {
|
||||||
|
AsyncImage(
|
||||||
|
model = ImageRequest.Builder(androidx.compose.ui.platform.LocalContext.current)
|
||||||
|
.data(agentAvatar)
|
||||||
|
.crossfade(true)
|
||||||
|
.build(),
|
||||||
|
contentDescription = "头像",
|
||||||
|
modifier = Modifier.size(18.dp),
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// Emoji or text avatar
|
||||||
|
agentAvatar != null -> {
|
||||||
|
Text(
|
||||||
|
text = agentAvatar,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
modifier = Modifier.size(18.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// Default fallback
|
||||||
|
else -> {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.SmartToy,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(16.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ class SettingsViewModel @Inject constructor(
|
|||||||
.stateIn(viewModelScope, SharingStarted.Eagerly, false)
|
.stateIn(viewModelScope, SharingStarted.Eagerly, false)
|
||||||
|
|
||||||
val ttsVoice: StateFlow<String> = tokenDataStore.ttsVoice
|
val ttsVoice: StateFlow<String> = tokenDataStore.ttsVoice
|
||||||
.stateIn(viewModelScope, SharingStarted.Eagerly, "alloy")
|
.stateIn(viewModelScope, SharingStarted.Eagerly, "xiaoxiao")
|
||||||
|
|
||||||
val themeMode: StateFlow<String> = tokenDataStore.themeMode
|
val themeMode: StateFlow<String> = tokenDataStore.themeMode
|
||||||
.stateIn(viewModelScope, SharingStarted.Eagerly, "system")
|
.stateIn(viewModelScope, SharingStarted.Eagerly, "system")
|
||||||
@@ -153,12 +153,21 @@ class SettingsViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun voiceLabel(voice: String): String = when (voice) {
|
fun voiceLabel(voice: String): String = when (voice) {
|
||||||
"alloy" -> "Alloy (中性)"
|
"xiaoxiao" -> "晓晓 (温柔女声)"
|
||||||
"echo" -> "Echo (男声)"
|
"xiaoyi" -> "晓怡 (活泼女声)"
|
||||||
"fable" -> "Fable (英式)"
|
"yunxi" -> "云希 (阳光男声)"
|
||||||
"onyx" -> "Onyx (深沉)"
|
"yunyang" -> "云扬 (新闻男声)"
|
||||||
"nova" -> "Nova (女声)"
|
"yunjian" -> "云健 (成熟男声)"
|
||||||
"shimmer" -> "Shimmer (轻柔)"
|
"yunxia" -> "云夏 (可爱女声)"
|
||||||
|
"xiaochen" -> "晓辰 (温婉女声)"
|
||||||
|
"xiaohan" -> "晓涵 (知性女声)"
|
||||||
|
// Backward compat: OpenAI → Edge voice names
|
||||||
|
"alloy" -> "晓晓 (默认)"
|
||||||
|
"echo" -> "云扬 (默认)"
|
||||||
|
"fable" -> "晓晓 (默认)"
|
||||||
|
"onyx" -> "云健 (默认)"
|
||||||
|
"nova" -> "晓怡 (默认)"
|
||||||
|
"shimmer" -> "晓晓 (默认)"
|
||||||
else -> voice
|
else -> voice
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -492,7 +501,7 @@ fun SettingsScreen(
|
|||||||
|
|
||||||
// Voice selection dialog
|
// Voice selection dialog
|
||||||
if (showVoiceDialog) {
|
if (showVoiceDialog) {
|
||||||
val voices = listOf("alloy", "echo", "fable", "onyx", "nova", "shimmer")
|
val voices = listOf("xiaoxiao", "xiaoyi", "yunxi", "yunyang", "yunjian", "yunxia")
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = { showVoiceDialog = false },
|
onDismissRequest = { showVoiceDialog = false },
|
||||||
title = { Text("选择 TTS 音色") },
|
title = { Text("选择 TTS 音色") },
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ class AudioRecorder @Inject constructor(
|
|||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun startRecording(): File {
|
fun startRecording(): File {
|
||||||
|
// Guard against double-start which leaks the first MediaRecorder
|
||||||
|
if (mediaRecorder != null) return outputFile!!
|
||||||
|
|
||||||
outputFile = File(context.cacheDir, "voice_${System.currentTimeMillis()}.aac")
|
outputFile = File(context.cacheDir, "voice_${System.currentTimeMillis()}.aac")
|
||||||
|
|
||||||
mediaRecorder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
mediaRecorder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ object MarkdownRenderer {
|
|||||||
* Uses regex to remove common formatting markers:
|
* Uses regex to remove common formatting markers:
|
||||||
* **bold**, *italic*, `code`, ```blocks```, # headings,
|
* **bold**, *italic*, `code`, ```blocks```, # headings,
|
||||||
* [links](url), , > blockquotes, - lists, | tables.
|
* [links](url), , > blockquotes, - lists, | tables.
|
||||||
|
* Also removes emoji and parenthetical content (same as web cleanForTTS).
|
||||||
*/
|
*/
|
||||||
fun stripMarkdownForTTS(text: String): String {
|
fun stripMarkdownForTTS(text: String): String {
|
||||||
if (text.isBlank()) return ""
|
if (text.isBlank()) return ""
|
||||||
@@ -47,19 +48,68 @@ object MarkdownRenderer {
|
|||||||
.replace(Regex("(?m)^>\\s*"), "") // blockquote
|
.replace(Regex("(?m)^>\\s*"), "") // blockquote
|
||||||
.replace(Regex("(?m)^---+$"), "") // horizontal rules
|
.replace(Regex("(?m)^---+$"), "") // horizontal rules
|
||||||
.replace("|", " ") // table pipes
|
.replace("|", " ") // table pipes
|
||||||
|
.replace(Regex("[((][^))]*[))]"), "") // parenthetical content
|
||||||
.replace(Regex("\\n{3,}"), "\n\n") // collapse excessive newlines
|
.replace(Regex("\\n{3,}"), "\n\n") // collapse excessive newlines
|
||||||
|
.let { stripEmoji(it) } // strip emoji characters
|
||||||
.trim()
|
.trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove emoji characters using code-point ranges.
|
||||||
|
* Java/Kotlin regex doesn't support Unicode emoji property escapes,
|
||||||
|
* so we filter by checking each code point against known emoji blocks.
|
||||||
|
*/
|
||||||
|
private fun stripEmoji(text: String): String {
|
||||||
|
val sb = StringBuilder(text.length)
|
||||||
|
var i = 0
|
||||||
|
while (i < text.length) {
|
||||||
|
val cp = text.codePointAt(i)
|
||||||
|
val charCount = Character.charCount(cp)
|
||||||
|
if (!isEmoji(cp)) {
|
||||||
|
sb.appendCodePoint(cp)
|
||||||
|
}
|
||||||
|
i += charCount
|
||||||
|
}
|
||||||
|
return sb.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isEmoji(cp: Int): Boolean {
|
||||||
|
return cp in 0x1F600..0x1F64F // Emoticons
|
||||||
|
|| cp in 0x1F300..0x1F5FF // Misc Symbols & Pictographs
|
||||||
|
|| cp in 0x1F680..0x1F6FF // Transport & Map
|
||||||
|
|| cp in 0x1F1E0..0x1F1FF // Flags (regional indicators)
|
||||||
|
|| cp in 0x1F900..0x1F9FF // Supplemental Symbols
|
||||||
|
|| cp in 0x1FA00..0x1FA6F // Chess Symbols
|
||||||
|
|| cp in 0x1FA70..0x1FAFF // Symbols Extended-A
|
||||||
|
|| cp in 0x2600..0x27BF // Misc Symbols + Dingbats
|
||||||
|
|| cp in 0x2300..0x23FF // Misc Technical
|
||||||
|
|| cp in 0x2B50..0x2B55 // Star/circle symbols
|
||||||
|
|| cp in 0x2702..0x27B0 // Scissors/dingbats
|
||||||
|
|| cp in 0x25AA..0x25FE // Geometric shapes
|
||||||
|
|| cp == 0x200D // Zero-width joiner
|
||||||
|
|| cp == 0xFE0F // Variation selector-16
|
||||||
|
|| cp in 0x2934..0x2935 // Curved arrows
|
||||||
|
|| cp == 0x3030 // Wavy dash
|
||||||
|
|| cp == 0x303D // Part alternation mark
|
||||||
|
|| cp == 0x3297 // Congratulation (CJK)
|
||||||
|
|| cp == 0x3299 // Secret (CJK)
|
||||||
|
|| cp in 0x1F000..0x1F02F // Mahjong/Domino tiles
|
||||||
|
|| cp in 0x1F0A0..0x1F0FF // Playing cards
|
||||||
|
|| cp in 0x1FA80..0x1FA8F // Kite, yo-yo etc
|
||||||
|
}
|
||||||
|
|
||||||
|
@Volatile
|
||||||
private var markwonInstance: Markwon? = null
|
private var markwonInstance: Markwon? = null
|
||||||
|
|
||||||
private fun getMarkwon(context: Context): Markwon {
|
private fun getMarkwon(context: Context): Markwon {
|
||||||
return markwonInstance ?: Markwon.builder(context)
|
return markwonInstance ?: synchronized(this) {
|
||||||
|
markwonInstance ?: Markwon.builder(context)
|
||||||
.usePlugin(CoilImagesPlugin.create(context))
|
.usePlugin(CoilImagesPlugin.create(context))
|
||||||
.usePlugin(LinkifyPlugin.create())
|
.usePlugin(LinkifyPlugin.create())
|
||||||
.build()
|
.build()
|
||||||
.also { markwonInstance = it }
|
.also { markwonInstance = it }
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Full Markwon-based markdown renderer wrapped in AndroidView.
|
* Full Markwon-based markdown renderer wrapped in AndroidView.
|
||||||
@@ -310,7 +360,7 @@ object MarkdownRenderer {
|
|||||||
withStyle(
|
withStyle(
|
||||||
SpanStyle(
|
SpanStyle(
|
||||||
fontFamily = FontFamily.Monospace,
|
fontFamily = FontFamily.Monospace,
|
||||||
background = androidx.compose.ui.graphics.Color(0x20000000)
|
background = MaterialTheme.colorScheme.surfaceVariant
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
append(text.substring(i + 1, end))
|
append(text.substring(i + 1, end))
|
||||||
|
|||||||
Reference in New Issue
Block a user