Merge remote-tracking branch 'origin/opti4' into opti6

This commit is contained in:
fengge 2026-06-16 19:39:17 +08:00
commit 8a7531428b
4 changed files with 54 additions and 53 deletions

View File

@ -21,7 +21,7 @@ import org.json.JSONArray
import org.json.JSONObject import org.json.JSONObject
import java.util.UUID import java.util.UUID
private const val STREAM_FLUSH_INTERVAL_MS = 50L private const val STREAM_FLUSH_INTERVAL_MS = 150L
class ChatViewModel(application: Application) : AndroidViewModel(application) { class ChatViewModel(application: Application) : AndroidViewModel(application) {
private val repository = AiChatRepository() private val repository = AiChatRepository()
@ -139,34 +139,37 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
} }
private fun saveCacheToLocal() { private fun saveCacheToLocal() {
try { // 将数据保存放到IO线程中防止SharedPreferences写大文本阻塞主线程卡顿
// 保存会话列表 viewModelScope.launch(kotlinx.coroutines.Dispatchers.IO) {
val sessionsArray = JSONArray() try {
_sessions.value.forEach { session -> // 保存会话列表
val obj = JSONObject() val sessionsArray = JSONArray()
obj.put("id", session.id) _sessions.value.forEach { session ->
obj.put("title", session.title) val obj = JSONObject()
obj.put("timestamp", session.timestamp) obj.put("id", session.id)
sessionsArray.put(obj) obj.put("title", session.title)
} obj.put("timestamp", session.timestamp)
sessionsArray.put(obj)
}
// 保存消息列表 // 保存消息列表
val messagesArray = JSONArray() val messagesArray = JSONArray()
_messages.value.forEach { msg -> _messages.value.forEach { msg ->
val obj = JSONObject() val obj = JSONObject()
obj.put("id", msg.id) obj.put("id", msg.id)
obj.put("sessionId", msg.sessionId) obj.put("sessionId", msg.sessionId)
obj.put("role", msg.role.name) obj.put("role", msg.role.name)
obj.put("content", msg.content) obj.put("content", msg.content)
messagesArray.put(obj) messagesArray.put(obj)
} }
prefs.edit() prefs.edit()
.putString("cache_sessions", sessionsArray.toString()) .putString("cache_sessions", sessionsArray.toString())
.putString("cache_messages", messagesArray.toString()) .putString("cache_messages", messagesArray.toString())
.apply() .apply()
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
}
} }
} }

View File

@ -94,25 +94,19 @@ fun FloatingChatWidget() {
visible = true visible = true
} }
// 👈 用 FAB 位置计算对话框位置,但强制钳制在屏幕内
// FAB右下角为基准对话框应该出现在 FAB 附近
val fabRight = screenWidthPx - paddingPx * 2 + fabOffsetX // FAB右边缘
val fabBottom = screenHeightPx - paddingPx * 2 - navBarPx + fabOffsetY // FAB下边缘
// 对话框优先放在 FAB 左上方,如果放不下就贴边
val rawX = fabRight - cardWidthPx - paddingPx // FAB左边
val rawY = fabBottom - cardHeightPx - paddingPx // FAB上边
// 👈 关键:钳制到屏幕内
val clampedX = rawX.coerceIn(0f, screenWidthPx - cardWidthPx)
val clampedY = rawY.coerceIn(0f, screenHeightPx - cardHeightPx)
AnimatedVisibility( AnimatedVisibility(
visible = visible, visible = visible,
enter = fadeIn(animationSpec = tween(300)) + scaleIn(initialScale = 0.8f, animationSpec = tween(300)), enter = fadeIn(animationSpec = tween(300)) + scaleIn(initialScale = 0.8f, animationSpec = tween(300)),
exit = fadeOut(animationSpec = tween(200)) + scaleOut(targetScale = 0.8f, animationSpec = tween(200)), exit = fadeOut(animationSpec = tween(200)) + scaleOut(targetScale = 0.8f, animationSpec = tween(200)),
modifier = Modifier modifier = Modifier
.offset { .offset {
val fabRight = screenWidthPx - paddingPx * 2 + fabOffsetX
val fabBottom = screenHeightPx - paddingPx * 2 - navBarPx + fabOffsetY
val rawX = fabRight - cardWidthPx - paddingPx
val rawY = fabBottom - cardHeightPx - paddingPx
val clampedX = rawX.coerceIn(0f, screenWidthPx - cardWidthPx)
val clampedY = rawY.coerceIn(0f, screenHeightPx - cardHeightPx)
IntOffset( IntOffset(
(clampedX + cardDragX).roundToInt() (clampedX + cardDragX).roundToInt()
.coerceIn(0, (screenWidthPx - cardWidthPx).toInt()), .coerceIn(0, (screenWidthPx - cardWidthPx).toInt()),

View File

@ -40,6 +40,18 @@ fun ChatArea(
} }
} }
val context = androidx.compose.ui.platform.LocalContext.current
val markwon = remember(context) {
io.noties.markwon.Markwon.builder(context)
.usePlugin(io.noties.markwon.core.CorePlugin.create())
.usePlugin(io.noties.markwon.ext.tables.TablePlugin.create(context))
.usePlugin(io.noties.markwon.html.HtmlPlugin.create())
.usePlugin(io.noties.markwon.linkify.LinkifyPlugin.create())
.usePlugin(io.noties.markwon.ext.strikethrough.StrikethroughPlugin.create())
.usePlugin(io.noties.markwon.ext.tasklist.TaskListPlugin.create(context))
.build()
}
val isAnyStreaming = remember(messages) { messages.any { it.isStreaming } } val isAnyStreaming = remember(messages) { messages.any { it.isStreaming } }
if (isAnyStreaming) { if (isAnyStreaming) {
val totalLength = messages.map { it.content.length }.sum() val totalLength = messages.map { it.content.length }.sum()
@ -111,6 +123,7 @@ fun ChatArea(
val contentWithCursor = msg.content + if (msg.isStreaming) "" else "" val contentWithCursor = msg.content + if (msg.isStreaming) "" else ""
MarkwonRenderer( MarkwonRenderer(
markdown = contentWithCursor, markdown = contentWithCursor,
markwon = markwon,
modifier = Modifier.padding(12.dp) modifier = Modifier.padding(12.dp)
) )
} }
@ -161,19 +174,7 @@ fun ChatArea(
} }
@Composable @Composable
fun MarkwonRenderer(markdown: String, modifier: Modifier) { fun MarkwonRenderer(markdown: String, markwon: io.noties.markwon.Markwon, modifier: Modifier) {
val context = androidx.compose.ui.platform.LocalContext.current
val markwon = remember(context) {
io.noties.markwon.Markwon.builder(context)
.usePlugin(io.noties.markwon.core.CorePlugin.create())
.usePlugin(io.noties.markwon.ext.tables.TablePlugin.create(context))
.usePlugin(io.noties.markwon.html.HtmlPlugin.create())
.usePlugin(io.noties.markwon.linkify.LinkifyPlugin.create())
.usePlugin(io.noties.markwon.ext.strikethrough.StrikethroughPlugin.create())
.usePlugin(io.noties.markwon.ext.tasklist.TaskListPlugin.create(context))
.build()
}
androidx.compose.ui.viewinterop.AndroidView( androidx.compose.ui.viewinterop.AndroidView(
factory = { ctx -> factory = { ctx ->
val textView = SafeSelectableTextView(ctx).apply { val textView = SafeSelectableTextView(ctx).apply {

View File

@ -187,6 +187,9 @@ object LogManager {
private val fileNameFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) private val fileNameFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
// 过滤掉高频的 VERBOSE 级别日志写入(例如录音帧),保留在控制台输出
if (priority == android.util.Log.VERBOSE) return
executor.execute { executor.execute {
try { try {
val logFile = File(logDir, "${fileNameFormat.format(Date())}.log") val logFile = File(logDir, "${fileNameFormat.format(Date())}.log")