Compare commits
5 Commits
d937ad854d
...
8a7531428b
| Author | SHA1 | Date |
|---|---|---|
|
|
8a7531428b | |
|
|
9ed1e31700 | |
|
|
e266a4e690 | |
|
|
89f1df9a9b | |
|
|
96068d56b2 |
|
|
@ -21,7 +21,7 @@ import org.json.JSONArray
|
|||
import org.json.JSONObject
|
||||
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) {
|
||||
private val repository = AiChatRepository()
|
||||
|
|
@ -139,34 +139,37 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||
}
|
||||
|
||||
private fun saveCacheToLocal() {
|
||||
try {
|
||||
// 保存会话列表
|
||||
val sessionsArray = JSONArray()
|
||||
_sessions.value.forEach { session ->
|
||||
val obj = JSONObject()
|
||||
obj.put("id", session.id)
|
||||
obj.put("title", session.title)
|
||||
obj.put("timestamp", session.timestamp)
|
||||
sessionsArray.put(obj)
|
||||
}
|
||||
// 将数据保存放到IO线程中,防止SharedPreferences写大文本阻塞主线程卡顿
|
||||
viewModelScope.launch(kotlinx.coroutines.Dispatchers.IO) {
|
||||
try {
|
||||
// 保存会话列表
|
||||
val sessionsArray = JSONArray()
|
||||
_sessions.value.forEach { session ->
|
||||
val obj = JSONObject()
|
||||
obj.put("id", session.id)
|
||||
obj.put("title", session.title)
|
||||
obj.put("timestamp", session.timestamp)
|
||||
sessionsArray.put(obj)
|
||||
}
|
||||
|
||||
// 保存消息列表
|
||||
val messagesArray = JSONArray()
|
||||
_messages.value.forEach { msg ->
|
||||
val obj = JSONObject()
|
||||
obj.put("id", msg.id)
|
||||
obj.put("sessionId", msg.sessionId)
|
||||
obj.put("role", msg.role.name)
|
||||
obj.put("content", msg.content)
|
||||
messagesArray.put(obj)
|
||||
}
|
||||
// 保存消息列表
|
||||
val messagesArray = JSONArray()
|
||||
_messages.value.forEach { msg ->
|
||||
val obj = JSONObject()
|
||||
obj.put("id", msg.id)
|
||||
obj.put("sessionId", msg.sessionId)
|
||||
obj.put("role", msg.role.name)
|
||||
obj.put("content", msg.content)
|
||||
messagesArray.put(obj)
|
||||
}
|
||||
|
||||
prefs.edit()
|
||||
.putString("cache_sessions", sessionsArray.toString())
|
||||
.putString("cache_messages", messagesArray.toString())
|
||||
.apply()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
prefs.edit()
|
||||
.putString("cache_sessions", sessionsArray.toString())
|
||||
.putString("cache_messages", messagesArray.toString())
|
||||
.apply()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -94,25 +94,19 @@ fun FloatingChatWidget() {
|
|||
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(
|
||||
visible = visible,
|
||||
enter = fadeIn(animationSpec = tween(300)) + scaleIn(initialScale = 0.8f, animationSpec = tween(300)),
|
||||
exit = fadeOut(animationSpec = tween(200)) + scaleOut(targetScale = 0.8f, animationSpec = tween(200)),
|
||||
modifier = Modifier
|
||||
.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(
|
||||
(clampedX + cardDragX).roundToInt()
|
||||
.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 } }
|
||||
if (isAnyStreaming) {
|
||||
val totalLength = messages.map { it.content.length }.sum()
|
||||
|
|
@ -111,6 +123,7 @@ fun ChatArea(
|
|||
val contentWithCursor = msg.content + if (msg.isStreaming) " █" else ""
|
||||
MarkwonRenderer(
|
||||
markdown = contentWithCursor,
|
||||
markwon = markwon,
|
||||
modifier = Modifier.padding(12.dp)
|
||||
)
|
||||
}
|
||||
|
|
@ -161,19 +174,7 @@ fun ChatArea(
|
|||
}
|
||||
|
||||
@Composable
|
||||
fun MarkwonRenderer(markdown: String, 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()
|
||||
}
|
||||
|
||||
fun MarkwonRenderer(markdown: String, markwon: io.noties.markwon.Markwon, modifier: Modifier) {
|
||||
androidx.compose.ui.viewinterop.AndroidView(
|
||||
factory = { ctx ->
|
||||
val textView = SafeSelectableTextView(ctx).apply {
|
||||
|
|
|
|||
|
|
@ -187,6 +187,9 @@ object LogManager {
|
|||
private val fileNameFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
|
||||
|
||||
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
|
||||
// 过滤掉高频的 VERBOSE 级别日志写入(例如录音帧),保留在控制台输出
|
||||
if (priority == android.util.Log.VERBOSE) return
|
||||
|
||||
executor.execute {
|
||||
try {
|
||||
val logFile = File(logDir, "${fileNameFormat.format(Date())}.log")
|
||||
|
|
|
|||
Loading…
Reference in New Issue