11package com.xjyzs.aiapi
22
33import android.annotation.SuppressLint
4- import android.content.ClipboardManager
54import android.content.Context
65import android.content.Intent
76import android.content.SharedPreferences
@@ -33,7 +32,6 @@ import androidx.compose.foundation.lazy.LazyColumn
3332import androidx.compose.foundation.lazy.itemsIndexed
3433import androidx.compose.foundation.rememberScrollState
3534import androidx.compose.foundation.shape.CircleShape
36- import androidx.compose.foundation.shape.RoundedCornerShape
3735import androidx.compose.foundation.text.selection.SelectionContainer
3836import androidx.compose.foundation.verticalScroll
3937import androidx.compose.material.icons.Icons
@@ -86,17 +84,10 @@ import androidx.compose.ui.layout.onSizeChanged
8684import androidx.compose.ui.platform.LocalContext
8785import androidx.compose.ui.res.stringResource
8886import androidx.compose.ui.res.vectorResource
89- import androidx.compose.ui.text.AnnotatedString
90- import androidx.compose.ui.text.SpanStyle
9187import androidx.compose.ui.text.TextRange
9288import androidx.compose.ui.text.TextStyle
93- import androidx.compose.ui.text.buildAnnotatedString
94- import androidx.compose.ui.text.font.FontFamily
95- import androidx.compose.ui.text.font.FontStyle
9689import androidx.compose.ui.text.font.FontWeight
9790import androidx.compose.ui.text.input.TextFieldValue
98- import androidx.compose.ui.text.style.TextDecoration
99- import androidx.compose.ui.unit.Dp
10091import androidx.compose.ui.unit.dp
10192import androidx.compose.ui.unit.sp
10293import androidx.lifecycle.Lifecycle
@@ -109,7 +100,9 @@ import com.google.gson.Gson
109100import com.google.gson.JsonParser
110101import com.google.gson.reflect.TypeToken
111102import com.xjyzs.aiapi.ui.theme.AIAPITheme
103+ import com.xjyzs.aiapi.utils.InlineMarkdown
112104import com.xjyzs.aiapi.utils.clickVibrate
105+ import com.xjyzs.aiapi.utils.hideKeyboard
113106import kotlinx.coroutines.Dispatchers
114107import kotlinx.coroutines.delay
115108import kotlinx.coroutines.launch
@@ -121,11 +114,6 @@ import okhttp3.RequestBody.Companion.toRequestBody
121114import java.io.BufferedReader
122115import java.io.InputStreamReader
123116import java.util.concurrent.TimeUnit
124- import androidx.compose.foundation.text.KeyboardActions
125- import androidx.compose.foundation.text.KeyboardOptions
126- import androidx.compose.ui.platform.LocalSoftwareKeyboardController
127- import androidx.compose.ui.platform.SoftwareKeyboardController
128- import androidx.compose.ui.text.input.ImeAction
129117
130118@Keep
131119data class Message (val role : String , val content : String )
@@ -278,7 +266,7 @@ fun MainUI(viewModel: ChatViewModel) {
278266 viewModel.addSystemMessage(systemPrompt)
279267 }
280268 delay(100 )
281- scrollState.animateScrollTo(contentHeight,tween(500 ))
269+ scrollState.animateScrollTo(contentHeight,tween(300 ))
282270 }
283271 LaunchedEffect (if (viewModel.msgs.isNotEmpty())viewModel.msgs.last().content.length else 0 ) {
284272 if (contentHeight - scrollState.value - containerHeight < 10 ) {
@@ -341,7 +329,7 @@ fun MainUI(viewModel: ChatViewModel) {
341329 }
342330 viewModel.currentSession= newName
343331 }
344- LocalSoftwareKeyboardController .current !! .hide ()
332+ context.hideKeyboard ()
345333 viewModel.addUserMessage(it)
346334 send(context, viewModel)
347335 viewModel.inputMsg = " "
@@ -723,7 +711,7 @@ private fun HistoryDrawer(viewModel: ChatViewModel, context: Context, sessionsPr
723711 scope.launch {
724712 scrollState.scrollTo(0 )
725713 delay(100 )
726- scrollState.animateScrollTo(contentHeight, tween(500 ))
714+ scrollState.animateScrollTo(contentHeight, tween(300 ))
727715 }
728716 }
729717 ) { Text (" 删除" ) }
@@ -793,7 +781,7 @@ private fun HistoryDrawer(viewModel: ChatViewModel, context: Context, sessionsPr
793781 scope.launch {
794782 scrollState.scrollTo(0 )
795783 delay(100 )
796- scrollState.animateScrollTo(contentHeight, tween(500 ))
784+ scrollState.animateScrollTo(contentHeight, tween(300 ))
797785 }
798786 }
799787 },
@@ -905,243 +893,4 @@ private fun createNewSession(viewModel: ChatViewModel,context: Context) {
905893 viewModel.currentSession = " 新对话" + System .currentTimeMillis().toString()
906894 viewModel.sessions.add(viewModel.currentSession)
907895 }
908- }
909-
910- class MarkdownParser {
911- // 代码块
912- private val codeBlockRegex = """ ```(.*?)\n([\s\S]*?)\s*```""" .toRegex()
913- // 块级元素正则表达式
914- private val headerRegex = """ ^(#{1,6})\s*(.*)""" .toRegex()
915- private val unorderedListRegex = """ ^[*+-]\s+(.*)""" .toRegex()
916- private val orderedListRegex = """ ^(\d+)\.\s+(.*)""" .toRegex()
917-
918- // 行内样式正则表达式
919- private val boldRegex = """ \*\*(.*?)\*\*""" .toRegex()
920- private val italicRegex = """ \*(.*?)\*""" .toRegex()
921- private val codeRegex = """ `(.*?)`""" .toRegex()
922- private val deleteRegex = """ ~~(.*?)~~""" .toRegex()
923-
924- fun parse (content : String ,context : Context ): List <@Composable () -> Unit > {
925- val blocks = mutableListOf< @Composable () -> Unit > ()
926-
927- var remainingText = content
928-
929- while (true ) {
930- val codeBlockMatch = codeBlockRegex.find(remainingText)
931-
932- val precedingText = if (codeBlockMatch != null ) {
933- remainingText.substring(0 , codeBlockMatch.range.first)
934- } else {
935- remainingText
936- }
937- parseRegularText(precedingText, blocks)
938- if (codeBlockMatch == null ) break
939- val (language, code) = codeBlockMatch.destructured
940- blocks.add { CodeBlock (code, language,context= context) }
941-
942- remainingText = remainingText.substring(codeBlockMatch.range.last + 1 )
943- }
944-
945- return blocks
946- }
947-
948- private fun parseRegularText (text : String , blocks : MutableList <@Composable () -> Unit >) {
949- text.split(" \n " ).forEach { line ->
950- when {
951- line.matches(headerRegex) -> parseHeader(line)?.let { blocks.add(it) }
952- line.matches(unorderedListRegex) -> parseListItem(line)?.let { blocks.add(it) }
953- line.matches(orderedListRegex) -> parseOrderedListItem(line)?.let {
954- blocks.add(it)
955- }
956-
957- else -> blocks.add { ParseInlineText (line) }
958- }
959- }
960- }
961-
962- @Composable
963- fun CodeBlock (
964- code : String ,
965- language : String = "",
966- modifier : Modifier = Modifier ,
967- backgroundColor : Color = MaterialTheme .colorScheme.surfaceContainerHighest,
968- cornerRadius : Dp = 8.dp,
969- context : Context
970- ) {
971- val typography = MaterialTheme .typography
972-
973- Surface (
974- modifier = modifier
975- .fillMaxWidth()
976- .padding(vertical = 8 .dp)
977- .clickable {
978- (context.getSystemService(Context .CLIPBOARD_SERVICE ) as ClipboardManager ).setText(
979- AnnotatedString (code)
980- )
981- },
982- shape = RoundedCornerShape (cornerRadius),
983- color = backgroundColor
984- ) {
985- Column {
986- if (language.isNotBlank()) {
987- Text (
988- text = language,
989- style = typography.labelSmall,
990- modifier = Modifier
991- .padding(start = 12 .dp, top = 8 .dp, end = 12 .dp)
992- .align(Alignment .Start ),
993- color = MaterialTheme .colorScheme.onSurfaceVariant
994- )
995- }
996- Text (
997- text = highlightSyntax(code, language),
998- style = typography.bodyMedium.copy(
999- fontFamily = FontFamily .Monospace ,
1000- lineHeight = 20 .sp
1001- ),
1002- modifier = Modifier
1003- .padding(12 .dp),
1004- color = MaterialTheme .colorScheme.onSurface
1005- )
1006- }
1007- }
1008- }
1009-
1010- fun highlightSyntax (code : String , language : String ): AnnotatedString {
1011- return buildAnnotatedString {
1012- append(code)
1013-
1014- val lc = language.lowercase()
1015- if (lc == " kotlin" || lc == " java" ) {
1016- val rules = listOf (
1017- Rule (""" val|var|class""" .toRegex(), SpanStyle (color = Color (0xFFFF9800 ))),
1018- )
1019- rules.forEach { rule ->
1020- rule.regex.findAll(code).forEach { match ->
1021- addStyle(rule.style, match.range.first, match.range.last + 1 )
1022- }
1023- }
1024- }
1025- else if (lc == " python" ) {
1026- val rules = listOf (
1027- Rule (""" print""" .toRegex(), SpanStyle (color = Color (0xFF16659B )))
1028- )
1029- rules.forEach { rule ->
1030- rule.regex.findAll(code).forEach { match ->
1031- addStyle(rule.style, match.range.first, match.range.last + 1 )
1032- }
1033- }
1034- }
1035- val rules = listOf (
1036- Rule (""" \b\d+(\.\d+)?\b""" .toRegex(), SpanStyle (color = Color (0xFF16659B ))), // 数字
1037- Rule (
1038- """ return |def |fun |try|except |except:|finally|with | as |if |else |import """ .toRegex(),
1039- SpanStyle (color = Color (0xFFFF9800 ))
1040- ),
1041- Rule (""" @.*?\n""" .toRegex(), SpanStyle (color = Color (0xFFFFEB3B ))),
1042- Rule (""" ".*?"|'.*?'""" .toRegex(), SpanStyle (color = Color (0xFF067D17 ))), // 字符串
1043- Rule (""" //.*|#.*""" .toRegex(), SpanStyle (color = Color (0xFF9E9E9E ))), // 注释
1044- Rule (""" /\*.*?\*/""" .toRegex(RegexOption .DOT_MATCHES_ALL ),SpanStyle (color = Color (0xFF9E9E9E ))), // 注释
1045- )
1046- rules.forEach { rule ->
1047- rule.regex.findAll(code).forEach { match ->
1048- addStyle(rule.style, match.range.first, match.range.last + 1 )
1049- }
1050- }
1051- }
1052- }
1053-
1054- private data class Rule (
1055- val regex : Regex ,
1056- val style : SpanStyle
1057- )
1058-
1059- private fun parseHeader (line : String ): (@Composable () -> Unit )? {
1060- val (hashes, text) = headerRegex.find(line)?.destructured ? : return null
1061- val level = hashes.length.coerceIn(1 , 6 )
1062- return {
1063- Text (
1064- text = parseInlineStyles(text),
1065- fontSize = when (level) {
1066- 1 -> 24 .sp
1067- else -> (24 - level * 1 ).sp
1068- },
1069- fontWeight = FontWeight .Bold
1070- )
1071- }
1072- }
1073-
1074- private fun parseListItem (line : String ): (@Composable () -> Unit )? {
1075- val (content) = unorderedListRegex.find(line)?.destructured ? : return null
1076- return {
1077- Row (verticalAlignment = Alignment .Top ) {
1078- Text (" • " , fontSize = 16 .sp)
1079- Text (parseInlineStyles(content))
1080- }
1081- }
1082- }
1083-
1084- private fun parseOrderedListItem (line : String ): (@Composable () -> Unit )? {
1085- val (num, content) = orderedListRegex.find(line)?.destructured ? : return null
1086- return {
1087- Row (verticalAlignment = Alignment .Top ) {
1088- Text (" $num . " , fontSize = 16 .sp)
1089- Text (parseInlineStyles(content))
1090- }
1091- }
1092- }
1093-
1094- @Composable
1095- private fun ParseInlineText (text : String ) {
1096- Text (parseInlineStyles(text))
1097- }
1098-
1099- @Composable
1100- private fun parseInlineStyles (text : String ): AnnotatedString {
1101- val annotatedString = buildAnnotatedString {
1102- append(text)
1103-
1104- // 处理粗体
1105- boldRegex.findAll(text).forEach { result ->
1106- addStyle(SpanStyle (fontWeight = FontWeight .Bold ), result.range.first+ 1 ,result.range.last)
1107- }
1108-
1109- // 处理斜体
1110- italicRegex.findAll(text).forEach { result ->
1111- addStyle(SpanStyle (fontStyle = FontStyle .Italic ), result.range.first+ 1 ,result.range.last)
1112- }
1113-
1114- // 处理删除线
1115- deleteRegex.findAll(text).forEach { result ->
1116- addStyle(SpanStyle (textDecoration = TextDecoration .LineThrough ), result.range.first+ 1 ,result.range.last)
1117- }
1118-
1119- // 处理代码样式
1120- codeRegex.findAll(text).forEach { result ->
1121- addStyle(
1122- SpanStyle (
1123- background = MaterialTheme .colorScheme.surfaceContainerHighest,
1124- fontFamily = FontFamily .Monospace
1125- ),
1126- result.range.first+ 1 ,result.range.last
1127- )
1128- }
1129- }
1130- return annotatedString
1131- }
1132- }
1133-
1134- @Composable
1135- fun InlineMarkdown (content : String , modifier : Modifier = Modifier ,context : Context ) {
1136- val parser = remember { MarkdownParser () }
1137- val blocks = remember(content) { parser.parse(content, context) }
1138-
1139- Column (modifier) {
1140- blocks.forEach { block ->
1141- Row {
1142- block()
1143- }
1144- Text (" \n " , lineHeight = 0 .sp)
1145- }
1146- }
1147- }
896+ }
0 commit comments