Merge remote-tracking branch 'origin/opti4' into opti6
This commit is contained in:
commit
8a7531428b
|
|
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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()),
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue