Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ android {
applicationId = "com.frischar.fantareal"
minSdk = 26
targetSdk = 34
versionCode = 14
versionName = "1.6.3"
versionCode = 15
versionName = "1.6.4"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ import com.frischar.fantareal.domain.worldbook.InjectionPosition
import com.frischar.fantareal.domain.worldbook.WorldbookEngine
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.JsonArray
import java.util.UUID

class ChatOrchestrator(
Expand Down Expand Up @@ -161,12 +164,71 @@ class ChatOrchestrator(
}
is LlmStreamEvent.Done -> {
val parsedContent = parseAssistantContent(currentContent)
val finalVisible = parsedContent.visible.ifBlank { "[Error: Empty assistant response]" }
conversationRepository.updateMessage(
id = assistantMsgId,
newContent = parsedContent.visible.ifBlank { "[Error: Empty assistant response]" },
newContent = finalVisible,
thinking = parsedContent.thinking,
saveToDisk = true
)

if (parsedContent.visible.isNotBlank() && settings.useSmartSplit) {
try {
val subagentPrompt = """
你是一个聊天输出后处理器。

你的任务是将输入文本整理成用于前端聊天气泡显示的内容。

规则:
1. 不得改写原文内容。
2. 不得新增剧情、补充描写或解释。
3. 不得润色文本。
4. 只移除明显不属于聊天正文的状态栏、系统栏、格式标签、空行和无意义分隔符。
5. 保留有效的角色动作、旁白、对白和剧情正文。
6. 按自然语义或句子进行切分。
7. 每个聊天气泡的内容之间,必须严格使用 `===` 作为唯一分隔符。
8. 只输出纯文本,绝对不要输出 JSON 或 Markdown,不要输出解释。

输入文本:
${parsedContent.visible}
""".trimIndent()
val subagentReq = LlmRequest(
model = settings.model,
system = "You are a text formatter. Output only plain text separated by ===.",
messages = listOf(com.frischar.fantareal.domain.llm.LlmMessage(role = "user", content = subagentPrompt)),
temperature = 0.1,
stream = true
)
var subagentResponse = ""
conversationRepository.updateMessageBubbles(assistantMsgId, listOf("子代理正在切分气泡..."), saveToDisk = false)

provider.stream(subagentReq).collect { event ->
if (event is LlmStreamEvent.Token) {
subagentResponse += event.text
val currentVisible = parseAssistantContent(subagentResponse).visible
val streamingBubbles = currentVisible.split("===")
.map { it.trim() }
.filter { it.isNotEmpty() }
if (streamingBubbles.isNotEmpty()) {
conversationRepository.updateMessageBubbles(assistantMsgId, streamingBubbles, saveToDisk = false)
}
}
}

val finalVisibleSubagent = parseAssistantContent(subagentResponse).visible
val finalBubbles = finalVisibleSubagent.split("===")
.map { it.trim() }
.filter { it.isNotEmpty() }

if (finalBubbles.isNotEmpty()) {
conversationRepository.updateMessageBubbles(assistantMsgId, finalBubbles, saveToDisk = true)
} else {
conversationRepository.updateMessageBubbles(assistantMsgId, listOf(finalVisible), saveToDisk = true)
}
} catch (e: Exception) {
conversationRepository.updateMessageBubbles(assistantMsgId, listOf(finalVisible), saveToDisk = true)
}
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,21 @@ class ConversationRepository(
}
}

suspend fun updateMessageBubbles(id: String, bubbles: List<String>, saveToDisk: Boolean = true) {
initialLoad.join()
if (saveToDisk) {
mutateMessages { current ->
current.map {
if (it.id == id) it.copy(bubbles = bubbles) else it
}
}
} else {
_messages.value = _messages.value.map {
if (it.id == id) it.copy(bubbles = bubbles) else it
}
}
}

suspend fun clearMessages() {
initialLoad.join()
mutateMessages { emptyList() }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.frischar.fantareal.data.repository
package com.frischar.fantareal.data.repository

import android.content.Context
import android.content.SharedPreferences
Expand All @@ -24,7 +24,8 @@ data class AppSettings(
val aiAvatarUri: String? = null,
val fontSize: Float = 16f,
val fontColor: String = "",
val splitRegex: String = ""
val splitRegex: String = "",
val useSmartSplit: Boolean = true
)

@Serializable
Expand All @@ -42,7 +43,8 @@ private data class StoredSettings(
val aiAvatarUri: String? = null,
val fontSize: Float = 16f,
val fontColor: String = "",
val splitRegex: String = ""
val splitRegex: String = "",
val useSmartSplit: Boolean = true
)

class SettingsRepository(
Expand Down Expand Up @@ -80,7 +82,8 @@ class SettingsRepository(
aiAvatarUri = storedSettings.aiAvatarUri,
fontSize = storedSettings.fontSize,
fontColor = storedSettings.fontColor,
splitRegex = storedSettings.splitRegex
splitRegex = storedSettings.splitRegex,
useSmartSplit = storedSettings.useSmartSplit
)
}

Expand All @@ -103,7 +106,8 @@ class SettingsRepository(
aiAvatarUri = newSettings.aiAvatarUri,
fontSize = newSettings.fontSize,
fontColor = newSettings.fontColor,
splitRegex = newSettings.splitRegex
splitRegex = newSettings.splitRegex,
useSmartSplit = newSettings.useSmartSplit
)
)
}
Expand All @@ -122,6 +126,7 @@ class SettingsRepository(
.putFloat("fontSize", newSettings.fontSize)
.putString("fontColor", newSettings.fontColor)
.putString("splitRegex", newSettings.splitRegex)
.putBoolean("useSmartSplit", newSettings.useSmartSplit)
.remove("apiKey")
.apply()
secretStore.saveApiKey(newSettings.apiKey)
Expand Down Expand Up @@ -160,7 +165,8 @@ class SettingsRepository(
aiAvatarUri = prefs.getString("aiAvatarUri", null),
fontSize = prefs.getFloat("fontSize", 16f),
fontColor = prefs.getString("fontColor", "") ?: "",
splitRegex = prefs.getString("splitRegex", "") ?: ""
splitRegex = prefs.getString("splitRegex", "") ?: "",
useSmartSplit = prefs.getBoolean("useSmartSplit", true)
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@ data class ConversationMessage(
val content: String,
val createdAt: Long,
val thinking: String? = null,
val error: Boolean = false
val error: Boolean = false,
val bubbles: List<String>? = null
)
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ fun AppScaffold() {
createdAtText = "",
thinkingText = it.thinking,
error = it.error,
bubbles = it.bubbles,
isStreaming = uiState.isSending && it.id == lastMsgId && it.role == com.frischar.fantareal.domain.chat.MessageRole.Assistant
)
}
Expand Down Expand Up @@ -211,6 +212,7 @@ fun AppScaffold() {
onFontSizeChange = settingsViewModel::updateFontSize,
onFontColorChange = settingsViewModel::updateFontColor,
onSplitRegexChange = settingsViewModel::updateSplitRegex,
onUseSmartSplitChange = settingsViewModel::updateUseSmartSplit,
onBackgroundImageUriChange = settingsViewModel::updateBackgroundImageUri,
onUserAvatarUriChange = settingsViewModel::updateUserAvatarUri,
onAiAvatarUriChange = settingsViewModel::updateAiAvatarUri,
Expand Down
11 changes: 9 additions & 2 deletions app/src/main/java/com/frischar/fantareal/ui/chat/ChatScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ data class ChatMessageUiModel(
val isStreaming: Boolean,
val spriteTag: String? = null,
val thinkingText: String? = null,
val error: Boolean = false
val error: Boolean = false,
val bubbles: List<String>? = null
)

@OptIn(ExperimentalMaterial3Api::class)
Expand Down Expand Up @@ -168,7 +169,13 @@ fun ChatScreen(personaName: String = "Fantareal",
} else {
Column {
val splitRegex = settings.splitRegex
val contents = if (splitRegex.isNotBlank()) {
val contents = if (settings.useSmartSplit) {
if (msg.bubbles.isNullOrEmpty()) {
if (msg.isStreaming) listOf("思考剧情中...") else listOf(msg.content)
} else {
msg.bubbles
}
} else if (splitRegex.isNotBlank()) {
try {
val parts = msg.content.split(Regex(splitRegex)).filter { it.isNotBlank() }
val mergedParts = mutableListOf<String>()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.frischar.fantareal.ui.settings
package com.frischar.fantareal.ui.settings

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
Expand Down Expand Up @@ -51,6 +51,7 @@ fun SettingsScreen(
onFontSizeChange: (Float) -> Unit,
onFontColorChange: (String) -> Unit,
onSplitRegexChange: (String) -> Unit,
onUseSmartSplitChange: (Boolean) -> Unit,
onBackgroundImageUriChange: (String?) -> Unit,
onUserAvatarUriChange: (String?) -> Unit,
onAiAvatarUriChange: (String?) -> Unit,
Expand Down Expand Up @@ -80,9 +81,11 @@ fun SettingsScreen(
fontSize = uiState.fontSize,
fontColor = uiState.fontColor,
splitRegex = uiState.splitRegex,
useSmartSplit = uiState.useSmartSplit,
onFontSizeChange = onFontSizeChange,
onFontColorChange = onFontColorChange,
onSplitRegexChange = onSplitRegexChange,
onUseSmartSplitChange = onUseSmartSplitChange,
onBackgroundImageUriChange = onBackgroundImageUriChange,
onUserAvatarUriChange = onUserAvatarUriChange,
onAiAvatarUriChange = onAiAvatarUriChange
Expand Down Expand Up @@ -120,6 +123,8 @@ fun AppearanceCard(
onFontSizeChange: (Float) -> Unit,
onFontColorChange: (String) -> Unit,
onSplitRegexChange: (String) -> Unit,
useSmartSplit: Boolean,
onUseSmartSplitChange: (Boolean) -> Unit,
darkMode: Boolean,
showAvatar: Boolean,
backgroundOpacity: Float,
Expand Down Expand Up @@ -232,13 +237,19 @@ fun AppearanceCard(
}
}
}
OutlinedTextField(
value = splitRegex,
onValueChange = onSplitRegexChange,
label = { Text("AI输出分段正则切割 (留空则不切割)") },
modifier = Modifier.fillMaxWidth(),
singleLine = true
)
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) {
Text("子代理切分消息")
Switch(checked = useSmartSplit, onCheckedChange = onUseSmartSplitChange)
}
if (!useSmartSplit) {
OutlinedTextField(
value = splitRegex,
onValueChange = onSplitRegexChange,
label = { Text("AI输出分段正则切割 (留空则不切割)") },
modifier = Modifier.fillMaxWidth(),
singleLine = true
)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.frischar.fantareal.ui.viewmodel
package com.frischar.fantareal.ui.viewmodel

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
Expand Down Expand Up @@ -27,6 +27,7 @@ data class SettingsUiState(
val fontSize: Float = 16f,
val fontColor: String = "",
val splitRegex: String = "",
val useSmartSplit: Boolean = true,
val saved: Boolean = false
)

Expand Down Expand Up @@ -94,6 +95,10 @@ class SettingsViewModel(
fun updateFontSize(value: Float) { _uiState.update { it.copy(fontSize = value, saved = false) } }
fun updateFontColor(value: String) { _uiState.update { it.copy(fontColor = value, saved = false) } }
fun updateSplitRegex(value: String) { _uiState.update { it.copy(splitRegex = value, saved = false) } }
fun updateUseSmartSplit(value: Boolean) {
_uiState.update { it.copy(useSmartSplit = value, saved = false) }
save()
}

fun save() {
val current = _uiState.value
Expand All @@ -114,7 +119,8 @@ class SettingsViewModel(
aiAvatarUri = current.aiAvatarUri,
fontSize = current.fontSize,
fontColor = current.fontColor,
splitRegex = current.splitRegex
splitRegex = current.splitRegex,
useSmartSplit = current.useSmartSplit
)
)
_uiState.update { settingsRepository.settings.value.toUiState(saved = true) }
Expand All @@ -138,6 +144,7 @@ class SettingsViewModel(
fontSize = fontSize,
fontColor = fontColor,
splitRegex = splitRegex,
useSmartSplit = useSmartSplit,
saved = saved
)
}
Expand Down
Loading