Merge branch 'honest-blowfish'
This commit is contained in:
@@ -0,0 +1,49 @@
|
|||||||
|
package ai.multica.app;
|
||||||
|
|
||||||
|
final class ChatExperiencePolicy {
|
||||||
|
private static final float CHAT_FOCUSED_COMPOSER_RATIO = 0.22f;
|
||||||
|
private static final float ISSUE_FOCUSED_COMPOSER_RATIO = 0.18f;
|
||||||
|
private static final int MIN_CHAT_FOCUSED_COMPOSER_HEIGHT = 132;
|
||||||
|
private static final int MAX_CHAT_FOCUSED_COMPOSER_HEIGHT = 184;
|
||||||
|
private static final int MIN_ISSUE_FOCUSED_COMPOSER_HEIGHT = 112;
|
||||||
|
private static final int MAX_ISSUE_FOCUSED_COMPOSER_HEIGHT = 152;
|
||||||
|
|
||||||
|
enum Route {
|
||||||
|
CHAT,
|
||||||
|
ISSUE_COMMENT,
|
||||||
|
ISSUE_REPLY
|
||||||
|
}
|
||||||
|
|
||||||
|
private ChatExperiencePolicy() {
|
||||||
|
}
|
||||||
|
|
||||||
|
static String newChatInitialTitle() {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean requiresTitleBeforeEnteringChat() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int focusedComposerHeightPx(Route route, int usableHeightPx) {
|
||||||
|
int height = Math.max(0, usableHeightPx);
|
||||||
|
if (route == Route.ISSUE_COMMENT || route == Route.ISSUE_REPLY) {
|
||||||
|
int target = Math.round(height * ISSUE_FOCUSED_COMPOSER_RATIO);
|
||||||
|
return clamp(target, MIN_ISSUE_FOCUSED_COMPOSER_HEIGHT, MAX_ISSUE_FOCUSED_COMPOSER_HEIGHT);
|
||||||
|
}
|
||||||
|
int target = Math.round(height * CHAT_FOCUSED_COMPOSER_RATIO);
|
||||||
|
return clamp(target, MIN_CHAT_FOCUSED_COMPOSER_HEIGHT, MAX_CHAT_FOCUSED_COMPOSER_HEIGHT);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int focusedComposerHeightPx(int usableHeightPx) {
|
||||||
|
return focusedComposerHeightPx(Route.CHAT, usableHeightPx);
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean isFocusedComposerRoute(Route route) {
|
||||||
|
return route == Route.CHAT || route == Route.ISSUE_COMMENT || route == Route.ISSUE_REPLY;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int clamp(int value, int min, int max) {
|
||||||
|
return Math.max(min, Math.min(max, value));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -111,6 +111,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
|||||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.platform.LocalFocusManager
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
@@ -390,6 +391,7 @@ private data class PilotIssueFormOptions(
|
|||||||
private data class PilotChatMessagesData(
|
private data class PilotChatMessagesData(
|
||||||
val messages: List<Models.ChatMessage>,
|
val messages: List<Models.ChatMessage>,
|
||||||
val pendingTask: Models.ChatPendingTask,
|
val pendingTask: Models.ChatPendingTask,
|
||||||
|
val agents: List<Models.Agent>,
|
||||||
)
|
)
|
||||||
|
|
||||||
private data class PilotChatSessionsData(
|
private data class PilotChatSessionsData(
|
||||||
@@ -1817,6 +1819,7 @@ private fun ComposePilotShell(
|
|||||||
session = activeChat,
|
session = activeChat,
|
||||||
zh = zh,
|
zh = zh,
|
||||||
onBack = { chatSession = null },
|
onBack = { chatSession = null },
|
||||||
|
onSessionChanged = { chatSession = it },
|
||||||
onArchived = {
|
onArchived = {
|
||||||
chatSession = null
|
chatSession = null
|
||||||
chatOpen = false
|
chatOpen = false
|
||||||
@@ -2968,32 +2971,6 @@ private fun InlineInboxAction(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun ChatNewSessionWebPanel(
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
content: @Composable ColumnScope.() -> Unit,
|
|
||||||
) {
|
|
||||||
val shape = RoundedCornerShape(MulticaVisualTokens.CardRadius)
|
|
||||||
Surface(
|
|
||||||
modifier = modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.semantics { contentDescription = "Chat New Session Web Panel" },
|
|
||||||
shape = shape,
|
|
||||||
color = MulticaColors.Surface,
|
|
||||||
tonalElevation = 0.dp,
|
|
||||||
shadowElevation = 0.dp,
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.border(1.dp, MulticaColors.Border.copy(alpha = 0.70f), shape)
|
|
||||||
.padding(horizontal = 12.dp, vertical = 11.dp),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
|
||||||
content = content,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ChatSessionListWebPanel(
|
private fun ChatSessionListWebPanel(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
@@ -3029,12 +3006,7 @@ private fun PilotChatSessions(
|
|||||||
) {
|
) {
|
||||||
var state by remember(workspaceId) { mutableStateOf<Result<PilotChatSessionsData>?>(null) }
|
var state by remember(workspaceId) { mutableStateOf<Result<PilotChatSessionsData>?>(null) }
|
||||||
var refresh by remember(workspaceId) { mutableIntStateOf(0) }
|
var refresh by remember(workspaceId) { mutableIntStateOf(0) }
|
||||||
var title by remember(workspaceId) { mutableStateOf("") }
|
|
||||||
var selectedAgentId by remember(workspaceId) { mutableStateOf<String?>(null) }
|
|
||||||
var agentPickerOpen by remember(workspaceId) { mutableStateOf(false) }
|
|
||||||
var agentQuery by remember(workspaceId) { mutableStateOf("") }
|
|
||||||
var showAllSessions by remember(workspaceId) { mutableStateOf(false) }
|
var showAllSessions by remember(workspaceId) { mutableStateOf(false) }
|
||||||
var showCreateForm by remember(workspaceId) { mutableStateOf(false) }
|
|
||||||
var creating by remember(workspaceId) { mutableStateOf(false) }
|
var creating by remember(workspaceId) { mutableStateOf(false) }
|
||||||
var deletingSessionId by remember(workspaceId) { mutableStateOf<String?>(null) }
|
var deletingSessionId by remember(workspaceId) { mutableStateOf<String?>(null) }
|
||||||
var actionText by remember(workspaceId) { mutableStateOf<String?>(null) }
|
var actionText by remember(workspaceId) { mutableStateOf<String?>(null) }
|
||||||
@@ -3053,24 +3025,17 @@ private fun PilotChatSessions(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
loaded.onSuccess { data ->
|
|
||||||
if (selectedAgentId == null || data.agents.none { it.id == selectedAgentId }) {
|
|
||||||
selectedAgentId = data.agents.firstOrNull()?.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
state = loaded
|
state = loaded
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createSession(data: PilotChatSessionsData) {
|
fun createSession(data: PilotChatSessionsData) {
|
||||||
if (creating) return
|
if (creating) return
|
||||||
val agent = data.agents.firstOrNull { it.id == selectedAgentId } ?: data.agents.firstOrNull()
|
val agent = data.agents.firstOrNull()
|
||||||
if (agent == null) {
|
if (agent == null) {
|
||||||
actionText = if (zh) "请先创建 Agent" else "Create an agent first"
|
actionText = if (zh) "请先创建 Agent" else "Create an agent first"
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val sessionTitle = title.trim().ifBlank {
|
val sessionTitle = ChatExperiencePolicy.newChatInitialTitle()
|
||||||
if (zh) "新的聊天" else "New Chat"
|
|
||||||
}
|
|
||||||
creating = true
|
creating = true
|
||||||
actionText = null
|
actionText = null
|
||||||
scope.launch {
|
scope.launch {
|
||||||
@@ -3079,9 +3044,9 @@ private fun PilotChatSessions(
|
|||||||
}
|
}
|
||||||
creating = false
|
creating = false
|
||||||
result.onSuccess { session ->
|
result.onSuccess { session ->
|
||||||
title = ""
|
|
||||||
actionText = if (zh) "聊天会话已创建" else "Chat session created"
|
actionText = if (zh) "聊天会话已创建" else "Chat session created"
|
||||||
refresh++
|
refresh++
|
||||||
|
onSessionClick(session)
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
actionText = "${if (zh) "创建聊天失败" else "Create chat failed"}: ${it.message ?: it.toString()}"
|
actionText = "${if (zh) "创建聊天失败" else "Create chat failed"}: ${it.message ?: it.toString()}"
|
||||||
}
|
}
|
||||||
@@ -3120,7 +3085,6 @@ private fun PilotChatSessions(
|
|||||||
) {
|
) {
|
||||||
val data = loaded.getOrThrow()
|
val data = loaded.getOrThrow()
|
||||||
val sessions = data.sessions
|
val sessions = data.sessions
|
||||||
val selectedAgent = data.agents.firstOrNull { it.id == selectedAgentId } ?: data.agents.firstOrNull()
|
|
||||||
val agentNamesById = data.agents.associate { it.id to it.name }
|
val agentNamesById = data.agents.associate { it.id to it.name }
|
||||||
item {
|
item {
|
||||||
Row(
|
Row(
|
||||||
@@ -3132,17 +3096,14 @@ private fun PilotChatSessions(
|
|||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
MulticaPillButton(
|
MulticaPillButton(
|
||||||
text = if (showCreateForm) {
|
text = if (creating) "..." else if (zh) "新聊天" else "New Chat",
|
||||||
if (zh) "收起新聊天" else "Hide New Chat"
|
|
||||||
} else {
|
|
||||||
if (zh) "新聊天" else "New Chat"
|
|
||||||
},
|
|
||||||
onClick = {
|
onClick = {
|
||||||
showCreateForm = !showCreateForm
|
createSession(data)
|
||||||
actionText = null
|
actionText = null
|
||||||
},
|
},
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
tone = if (showCreateForm) MulticaButtonTone.Secondary else MulticaButtonTone.Primary,
|
enabled = !creating,
|
||||||
|
tone = MulticaButtonTone.Primary,
|
||||||
contentDescription = "Chat New Session Toggle",
|
contentDescription = "Chat New Session Toggle",
|
||||||
)
|
)
|
||||||
MulticaPillButton(
|
MulticaPillButton(
|
||||||
@@ -3161,95 +3122,6 @@ private fun PilotChatSessions(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (showCreateForm) {
|
|
||||||
item {
|
|
||||||
SettingsSectionLabel(if (zh) "新聊天" else "New Chat")
|
|
||||||
ChatNewSessionWebPanel(
|
|
||||||
modifier = Modifier.semantics { contentDescription = "Chat New Session Web Panel" },
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.semantics(mergeDescendants = false) { contentDescription = "Chat New Session Web Form" },
|
|
||||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
|
||||||
) {
|
|
||||||
MulticaTextField(
|
|
||||||
value = title,
|
|
||||||
onValueChange = { title = it },
|
|
||||||
label = if (zh) "标题" else "Title",
|
|
||||||
contentDescription = "Chat New Session Title",
|
|
||||||
singleLine = true,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(top = 2.dp),
|
|
||||||
)
|
|
||||||
ChatAgentPickerRow(
|
|
||||||
selectedAgent = selectedAgent,
|
|
||||||
expanded = agentPickerOpen,
|
|
||||||
zh = zh,
|
|
||||||
onClick = {
|
|
||||||
if (data.agents.isNotEmpty()) {
|
|
||||||
agentPickerOpen = !agentPickerOpen
|
|
||||||
actionText = null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (agentPickerOpen) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.fillMaxWidth().padding(vertical = 10.dp),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(0.dp),
|
|
||||||
) {
|
|
||||||
MulticaTextField(
|
|
||||||
value = agentQuery,
|
|
||||||
onValueChange = { agentQuery = it },
|
|
||||||
label = if (zh) "搜索 Agent" else "Search Agents",
|
|
||||||
contentDescription = "Chat Agent Search",
|
|
||||||
singleLine = true,
|
|
||||||
modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp),
|
|
||||||
)
|
|
||||||
val filteredAgents = data.agents.filter { agent ->
|
|
||||||
val haystack = "${agent.name}\n${agent.description}\n${agent.id}".lowercase()
|
|
||||||
haystack.contains(agentQuery.trim().lowercase())
|
|
||||||
}
|
|
||||||
if (filteredAgents.isEmpty()) {
|
|
||||||
Text(
|
|
||||||
text = if (zh) "没有匹配的 Agent" else "No matching agents",
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = MulticaColors.Muted,
|
|
||||||
modifier = Modifier.padding(vertical = 6.dp),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
filteredAgents.take(20).forEachIndexed { index, agent ->
|
|
||||||
ChatAgentWebDenseRow(
|
|
||||||
agent = agent,
|
|
||||||
selected = agent.id == selectedAgent?.id,
|
|
||||||
showDivider = index < filteredAgents.lastIndex && index < 19,
|
|
||||||
onClick = {
|
|
||||||
selectedAgentId = agent.id
|
|
||||||
agentPickerOpen = false
|
|
||||||
agentQuery = ""
|
|
||||||
actionText = null
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(top = 10.dp, bottom = 12.dp),
|
|
||||||
horizontalArrangement = Arrangement.End,
|
|
||||||
) {
|
|
||||||
MulticaPillButton(
|
|
||||||
text = if (creating) "..." else if (zh) "创建聊天" else "Create Chat",
|
|
||||||
onClick = { createSession(data) },
|
|
||||||
tone = MulticaButtonTone.Primary,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!actionText.isNullOrBlank()) {
|
if (!actionText.isNullOrBlank()) {
|
||||||
item {
|
item {
|
||||||
val isError = actionText!!.contains("failed", true) || actionText!!.contains("失败")
|
val isError = actionText!!.contains("failed", true) || actionText!!.contains("失败")
|
||||||
@@ -3544,10 +3416,12 @@ private fun PilotChatMessages(
|
|||||||
session: Models.ChatSession,
|
session: Models.ChatSession,
|
||||||
zh: Boolean,
|
zh: Boolean,
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
|
onSessionChanged: (Models.ChatSession) -> Unit,
|
||||||
onArchived: () -> Unit,
|
onArchived: () -> Unit,
|
||||||
) {
|
) {
|
||||||
var state by remember(session.id) { mutableStateOf<Result<PilotChatMessagesData>?>(null) }
|
var state by remember(session.id) { mutableStateOf<Result<PilotChatMessagesData>?>(null) }
|
||||||
var refresh by remember(session.id) { mutableIntStateOf(0) }
|
var messagesRefresh by remember(session.id) { mutableIntStateOf(0) }
|
||||||
|
var pendingRefresh by remember(session.id) { mutableIntStateOf(0) }
|
||||||
var draft by remember(session.id) { mutableStateOf("") }
|
var draft by remember(session.id) { mutableStateOf("") }
|
||||||
var sending by remember(session.id) { mutableStateOf(false) }
|
var sending by remember(session.id) { mutableStateOf(false) }
|
||||||
var locallyPending by remember(session.id) { mutableStateOf(false) }
|
var locallyPending by remember(session.id) { mutableStateOf(false) }
|
||||||
@@ -3555,31 +3429,65 @@ private fun PilotChatMessages(
|
|||||||
var archiving by remember(session.id) { mutableStateOf(false) }
|
var archiving by remember(session.id) { mutableStateOf(false) }
|
||||||
var cancelling by remember(session.id) { mutableStateOf(false) }
|
var cancelling by remember(session.id) { mutableStateOf(false) }
|
||||||
var headerMenuOpen by remember(session.id) { mutableStateOf(false) }
|
var headerMenuOpen by remember(session.id) { mutableStateOf(false) }
|
||||||
|
var agentPickerOpen by remember(session.id) { mutableStateOf(false) }
|
||||||
|
var agentQuery by remember(session.id) { mutableStateOf("") }
|
||||||
|
var switchingAgentId by remember(session.id) { mutableStateOf<String?>(null) }
|
||||||
|
var composerFocused by remember(session.id) { mutableStateOf(false) }
|
||||||
var actionError by remember(session.id) { mutableStateOf<String?>(null) }
|
var actionError by remember(session.id) { mutableStateOf<String?>(null) }
|
||||||
|
val configuration = LocalConfiguration.current
|
||||||
|
val focusedComposerHeight = ChatExperiencePolicy.focusedComposerHeightPx(
|
||||||
|
ChatExperiencePolicy.Route.CHAT,
|
||||||
|
configuration.screenHeightDp,
|
||||||
|
).dp
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val hazeState = rememberHazeState()
|
val hazeState = rememberHazeState()
|
||||||
val haptic = LocalHapticFeedback.current
|
val haptic = LocalHapticFeedback.current
|
||||||
|
|
||||||
LaunchedEffect(session.id, refresh) {
|
LaunchedEffect(session.id, messagesRefresh) {
|
||||||
state = null
|
val previous = state
|
||||||
state = withContext(Dispatchers.IO) {
|
val result = withContext(Dispatchers.IO) {
|
||||||
runCatching {
|
runCatching {
|
||||||
val messages = api.chatMessages(workspaceId, session.id)
|
val messages = api.chatMessages(workspaceId, session.id)
|
||||||
val pending = runCatching { api.pendingChatTask(workspaceId, session.id) }
|
val pending = runCatching { api.pendingChatTask(workspaceId, session.id) }
|
||||||
.getOrElse { Models.ChatPendingTask(org.json.JSONObject()) }
|
.getOrElse { Models.ChatPendingTask(org.json.JSONObject()) }
|
||||||
|
val agents = api.agents(workspaceId)
|
||||||
|
.filter { it.archivedAt.isBlank() }
|
||||||
|
.sortedBy { it.name.lowercase() }
|
||||||
runCatching { api.markChatSessionRead(workspaceId, session.id) }
|
runCatching { api.markChatSessionRead(workspaceId, session.id) }
|
||||||
PilotChatMessagesData(messages, pending)
|
PilotChatMessagesData(messages, pending, agents)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (result.isSuccess || previous == null || previous.isFailure) {
|
||||||
|
state = result
|
||||||
|
} else {
|
||||||
|
actionError = "${if (zh) "消息刷新失败" else "Message refresh failed"}: ${result.exceptionOrNull()?.message ?: result.exceptionOrNull().toString()}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(sending, state?.getOrNull()?.pendingTask?.taskId) {
|
LaunchedEffect(session.id, pendingRefresh) {
|
||||||
|
val current = state?.getOrNull() ?: return@LaunchedEffect
|
||||||
|
val previousPendingTaskId = current.pendingTask.taskId
|
||||||
|
val result = withContext(Dispatchers.IO) {
|
||||||
|
runCatching { api.pendingChatTask(workspaceId, session.id) }
|
||||||
|
}
|
||||||
|
result.onSuccess { pending ->
|
||||||
|
val latest = state?.getOrNull() ?: current
|
||||||
|
state = Result.success(latest.copy(pendingTask = pending))
|
||||||
|
if (validChatTaskId(previousPendingTaskId) && !validChatTaskId(pending.taskId)) {
|
||||||
|
messagesRefresh++
|
||||||
|
}
|
||||||
|
}.onFailure {
|
||||||
|
actionError = "${if (zh) "任务状态刷新失败" else "Task status refresh failed"}: ${it.message ?: it.toString()}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(sending, locallyPending, state?.getOrNull()?.pendingTask?.taskId) {
|
||||||
val taskId = state?.getOrNull()?.pendingTask?.taskId.orEmpty()
|
val taskId = state?.getOrNull()?.pendingTask?.taskId.orEmpty()
|
||||||
if (!sending && taskId.isBlank()) return@LaunchedEffect
|
if (!sending && !locallyPending && !validChatTaskId(taskId)) return@LaunchedEffect
|
||||||
while (sending || taskId.isNotBlank()) {
|
while (sending || locallyPending || validChatTaskId(taskId)) {
|
||||||
delay(5_000)
|
delay(2_000)
|
||||||
refresh++
|
pendingRefresh++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3641,11 +3549,43 @@ private fun PilotChatMessages(
|
|||||||
runCatching { api.cancelTaskById(workspaceId, task.taskId) }
|
runCatching { api.cancelTaskById(workspaceId, task.taskId) }
|
||||||
}
|
}
|
||||||
cancelling = false
|
cancelling = false
|
||||||
result.onSuccess { refresh++ }
|
result.onSuccess {
|
||||||
|
pendingRefresh++
|
||||||
|
messagesRefresh++
|
||||||
|
}
|
||||||
.onFailure { actionError = "${if (zh) "取消失败" else "Cancel failed"}: ${it.message ?: it.toString()}" }
|
.onFailure { actionError = "${if (zh) "取消失败" else "Cancel failed"}: ${it.message ?: it.toString()}" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun switchAgent(agent: Models.Agent) {
|
||||||
|
if (agent.id == session.agentId || switchingAgentId != null) {
|
||||||
|
agentPickerOpen = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switchingAgentId = agent.id
|
||||||
|
actionError = null
|
||||||
|
haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove)
|
||||||
|
scope.launch {
|
||||||
|
val result = withContext(Dispatchers.IO) {
|
||||||
|
runCatching {
|
||||||
|
api.createChatSession(
|
||||||
|
workspaceId,
|
||||||
|
agent.id,
|
||||||
|
ChatExperiencePolicy.newChatInitialTitle(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switchingAgentId = null
|
||||||
|
result.onSuccess { next ->
|
||||||
|
agentPickerOpen = false
|
||||||
|
agentQuery = ""
|
||||||
|
onSessionChanged(next)
|
||||||
|
}.onFailure {
|
||||||
|
actionError = "${if (zh) "切换 Agent 失败" else "Switch agent failed"}: ${it.message ?: it.toString()}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@@ -3711,6 +3651,57 @@ private fun PilotChatMessages(
|
|||||||
modifier = Modifier.padding(horizontal = 18.dp),
|
modifier = Modifier.padding(horizontal = 18.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
loaded?.getOrNull()?.let { chatData ->
|
||||||
|
val activeAgent = chatData.agents.firstOrNull { it.id == session.agentId } ?: chatData.agents.firstOrNull()
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 18.dp, vertical = 8.dp)
|
||||||
|
.semantics(mergeDescendants = false) { contentDescription = "Chat Detail Agent Selector" },
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
ChatAgentPickerRow(
|
||||||
|
selectedAgent = activeAgent,
|
||||||
|
expanded = agentPickerOpen,
|
||||||
|
zh = zh,
|
||||||
|
onClick = {
|
||||||
|
if (chatData.agents.isNotEmpty() && switchingAgentId == null) {
|
||||||
|
agentPickerOpen = !agentPickerOpen
|
||||||
|
actionError = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if (agentPickerOpen) {
|
||||||
|
MulticaTextField(
|
||||||
|
value = agentQuery,
|
||||||
|
onValueChange = { agentQuery = it },
|
||||||
|
label = if (zh) "搜索 Agent" else "Search Agents",
|
||||||
|
contentDescription = "Chat Detail Agent Search",
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
val filteredAgents = chatData.agents.filter { agent ->
|
||||||
|
val haystack = "${agent.name}\n${agent.description}\n${agent.id}".lowercase()
|
||||||
|
haystack.contains(agentQuery.trim().lowercase())
|
||||||
|
}
|
||||||
|
if (filteredAgents.isEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = if (zh) "没有匹配的 Agent" else "No matching agents",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MulticaColors.Muted,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
filteredAgents.take(20).forEachIndexed { index, agent ->
|
||||||
|
ChatAgentWebDenseRow(
|
||||||
|
agent = agent,
|
||||||
|
selected = agent.id == activeAgent?.id,
|
||||||
|
showDivider = index < filteredAgents.lastIndex && index < 19,
|
||||||
|
onClick = { switchAgent(agent) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!actionError.isNullOrBlank()) {
|
if (!actionError.isNullOrBlank()) {
|
||||||
PilotInlineResultState(
|
PilotInlineResultState(
|
||||||
message = actionError.orEmpty(),
|
message = actionError.orEmpty(),
|
||||||
@@ -3736,7 +3727,7 @@ private fun PilotChatMessages(
|
|||||||
) {
|
) {
|
||||||
MulticaErrorState(
|
MulticaErrorState(
|
||||||
message = "${if (zh) "消息加载失败" else "Messages failed"}\n${loaded.exceptionOrNull()?.message.orEmpty()}",
|
message = "${if (zh) "消息加载失败" else "Messages failed"}\n${loaded.exceptionOrNull()?.message.orEmpty()}",
|
||||||
onRetry = { refresh++ },
|
onRetry = { messagesRefresh++ },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
else -> LazyColumn(
|
else -> LazyColumn(
|
||||||
@@ -3757,23 +3748,25 @@ private fun PilotChatMessages(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
items(messages) { message ->
|
items(
|
||||||
ChatMessageBubble(message = message, zh = zh)
|
items = messages,
|
||||||
if (validChatTaskId(message.taskId)) {
|
key = { message ->
|
||||||
ChatTaskTimeline(
|
message.id.ifBlank { "${message.role}:${message.createdAt}:${message.content.hashCode()}" }
|
||||||
api = api,
|
},
|
||||||
workspaceId = workspaceId,
|
) { message ->
|
||||||
taskId = message.taskId,
|
ChatMessageBubble(message = message, zh = zh, api = api, workspaceId = workspaceId)
|
||||||
zh = zh,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
val activePendingTaskId = activePending.taskId
|
||||||
|
val pendingAlreadyPersisted = validChatTaskId(activePendingTaskId) &&
|
||||||
|
messages.any { message ->
|
||||||
|
!message.role.equals("user", ignoreCase = true) && message.taskId == activePendingTaskId
|
||||||
}
|
}
|
||||||
if (sending || locallyPending || validChatTaskId(activePending.taskId)) {
|
if ((sending || locallyPending || validChatTaskId(activePendingTaskId)) && !pendingAlreadyPersisted) {
|
||||||
item(key = "chat-latest-progress") {
|
item(key = "chat-latest-progress") {
|
||||||
ChatPendingTimelineCard(
|
ChatPendingTimelineCard(
|
||||||
api = api,
|
api = api,
|
||||||
workspaceId = workspaceId,
|
workspaceId = workspaceId,
|
||||||
taskId = activePending.taskId,
|
taskId = activePendingTaskId,
|
||||||
status = activePending.status.ifBlank { if (sending || locallyPending) "queued" else "" },
|
status = activePending.status.ifBlank { if (sending || locallyPending) "queued" else "" },
|
||||||
zh = zh,
|
zh = zh,
|
||||||
)
|
)
|
||||||
@@ -3800,7 +3793,15 @@ private fun PilotChatMessages(
|
|||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 18.dp, vertical = 10.dp),
|
.heightIn(min = if (composerFocused) focusedComposerHeight else 0.dp)
|
||||||
|
.padding(horizontal = 18.dp, vertical = 10.dp)
|
||||||
|
.semantics {
|
||||||
|
contentDescription = if (composerFocused) {
|
||||||
|
"Chat Focused Composer Expanded"
|
||||||
|
} else {
|
||||||
|
"Chat Compact Composer"
|
||||||
|
}
|
||||||
|
},
|
||||||
shape = RoundedCornerShape(14.dp),
|
shape = RoundedCornerShape(14.dp),
|
||||||
color = MulticaColors.Surface.copy(alpha = 0.94f),
|
color = MulticaColors.Surface.copy(alpha = 0.94f),
|
||||||
tonalElevation = 0.dp,
|
tonalElevation = 0.dp,
|
||||||
@@ -3820,8 +3821,9 @@ private fun PilotChatMessages(
|
|||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
label = if (zh) "发送 Markdown 消息..." else "Send a Markdown message...",
|
label = if (zh) "发送 Markdown 消息..." else "Send a Markdown message...",
|
||||||
contentDescription = "Chat Message Input",
|
contentDescription = "Chat Message Input",
|
||||||
minLines = 1,
|
minLines = if (composerFocused) 4 else 1,
|
||||||
maxLines = 4,
|
maxLines = if (composerFocused) 8 else 4,
|
||||||
|
onFocusedChange = { composerFocused = it },
|
||||||
)
|
)
|
||||||
MulticaIconPillButton(
|
MulticaIconPillButton(
|
||||||
icon = Icons.AutoMirrored.Outlined.Send,
|
icon = Icons.AutoMirrored.Outlined.Send,
|
||||||
@@ -3840,7 +3842,8 @@ private fun PilotChatMessages(
|
|||||||
sending = false
|
sending = false
|
||||||
result.onSuccess {
|
result.onSuccess {
|
||||||
draft = ""
|
draft = ""
|
||||||
refresh++
|
pendingRefresh++
|
||||||
|
messagesRefresh++
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
locallyPending = false
|
locallyPending = false
|
||||||
sendError = "${if (zh) "发送失败" else "Send failed"}: ${it.message ?: it.toString()}"
|
sendError = "${if (zh) "发送失败" else "Send failed"}: ${it.message ?: it.toString()}"
|
||||||
@@ -3920,6 +3923,8 @@ private fun ChatWindowWebEmptyState(
|
|||||||
private fun ChatMessageBubble(
|
private fun ChatMessageBubble(
|
||||||
message: Models.ChatMessage,
|
message: Models.ChatMessage,
|
||||||
zh: Boolean,
|
zh: Boolean,
|
||||||
|
api: ApiClient? = null,
|
||||||
|
workspaceId: String? = null,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val haptic = LocalHapticFeedback.current
|
val haptic = LocalHapticFeedback.current
|
||||||
@@ -4029,10 +4034,23 @@ private fun ChatPendingTimelineCard(
|
|||||||
status: String,
|
status: String,
|
||||||
zh: Boolean,
|
zh: Boolean,
|
||||||
) {
|
) {
|
||||||
Column(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.semantics { contentDescription = "Chat Pending Timeline" },
|
.semantics { contentDescription = "Chat Message Bubble assistant streaming Chat Pending Timeline" },
|
||||||
|
horizontalArrangement = Arrangement.Start,
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier.fillMaxWidth(0.84f),
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
color = MulticaColors.Surface,
|
||||||
|
tonalElevation = 0.dp,
|
||||||
|
shadowElevation = 0.dp,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.border(1.dp, MulticaColors.Border, RoundedCornerShape(12.dp))
|
||||||
|
.padding(horizontal = 10.dp, vertical = 9.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
) {
|
) {
|
||||||
ChatTaskWebLiveBanner(
|
ChatTaskWebLiveBanner(
|
||||||
@@ -4043,30 +4061,10 @@ private fun ChatPendingTimelineCard(
|
|||||||
active = true,
|
active = true,
|
||||||
)
|
)
|
||||||
if (taskId.isNotBlank()) {
|
if (taskId.isNotBlank()) {
|
||||||
ChatTaskTimelineRows(api = api, workspaceId = workspaceId, taskId = taskId, zh = zh)
|
ChatTaskTimelineRows(api = api, workspaceId = workspaceId, taskId = taskId, zh = zh, active = true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun ChatTaskTimeline(
|
|
||||||
api: ApiClient,
|
|
||||||
workspaceId: String,
|
|
||||||
taskId: String,
|
|
||||||
zh: Boolean,
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.semantics { contentDescription = "Chat Task Timeline $taskId" },
|
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
|
||||||
) {
|
|
||||||
ChatTaskWebLiveBanner(
|
|
||||||
title = if (zh) "Agent 执行过程" else "Agent timeline",
|
|
||||||
subtitle = Models.shortId(taskId),
|
|
||||||
active = false,
|
|
||||||
)
|
|
||||||
ChatTaskTimelineRows(api = api, workspaceId = workspaceId, taskId = taskId, zh = zh)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4076,10 +4074,16 @@ private fun ChatTaskTimelineRows(
|
|||||||
workspaceId: String,
|
workspaceId: String,
|
||||||
taskId: String,
|
taskId: String,
|
||||||
zh: Boolean,
|
zh: Boolean,
|
||||||
|
active: Boolean,
|
||||||
) {
|
) {
|
||||||
var state by remember(taskId, workspaceId) { mutableStateOf<Result<List<Models.TaskMessage>>?>(null) }
|
var state by remember(taskId, workspaceId) { mutableStateOf<Result<List<Models.TaskMessage>>?>(null) }
|
||||||
LaunchedEffect(taskId, workspaceId) {
|
LaunchedEffect(taskId, workspaceId, active) {
|
||||||
|
if (taskId.isBlank()) return@LaunchedEffect
|
||||||
|
do {
|
||||||
state = withContext(Dispatchers.IO) { runCatching { api.runMessages(taskId, workspaceId) } }
|
state = withContext(Dispatchers.IO) { runCatching { api.runMessages(taskId, workspaceId) } }
|
||||||
|
if (!active) break
|
||||||
|
delay(1_500)
|
||||||
|
} while (active)
|
||||||
}
|
}
|
||||||
when (val loaded = state) {
|
when (val loaded = state) {
|
||||||
null -> ChatTaskWebEmptyRow(
|
null -> ChatTaskWebEmptyRow(
|
||||||
@@ -10776,6 +10780,12 @@ private fun IssueCommentReplyBox(
|
|||||||
) {
|
) {
|
||||||
var mentionPickerOpen by remember(parentId) { mutableStateOf(false) }
|
var mentionPickerOpen by remember(parentId) { mutableStateOf(false) }
|
||||||
var mentionQuery by remember(parentId) { mutableStateOf("") }
|
var mentionQuery by remember(parentId) { mutableStateOf("") }
|
||||||
|
var replyFocused by remember(parentId) { mutableStateOf(false) }
|
||||||
|
val configuration = LocalConfiguration.current
|
||||||
|
val focusedComposerHeight = ChatExperiencePolicy.focusedComposerHeightPx(
|
||||||
|
ChatExperiencePolicy.Route.ISSUE_REPLY,
|
||||||
|
configuration.screenHeightDp,
|
||||||
|
).dp
|
||||||
val focusRequester = remember(parentId) { FocusRequester() }
|
val focusRequester = remember(parentId) { FocusRequester() }
|
||||||
val keyboard = LocalSoftwareKeyboardController.current
|
val keyboard = LocalSoftwareKeyboardController.current
|
||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
@@ -10819,8 +10829,15 @@ val replyActionModifier = Modifier.size(42.dp)
|
|||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
.heightIn(min = if (replyFocused) focusedComposerHeight else 0.dp)
|
||||||
.padding(start = if (bottomMode) 0.dp else 38.dp, bottom = if (bottomMode) 0.dp else 8.dp)
|
.padding(start = if (bottomMode) 0.dp else 38.dp, bottom = if (bottomMode) 0.dp else 8.dp)
|
||||||
.semantics(mergeDescendants = false) { contentDescription = "Issue Comment Web Reply Editor $parentId" },
|
.semantics(mergeDescendants = false) {
|
||||||
|
contentDescription = if (replyFocused) {
|
||||||
|
"Issue Comment Web Reply Editor Expanded $parentId"
|
||||||
|
} else {
|
||||||
|
"Issue Comment Web Reply Editor $parentId"
|
||||||
|
}
|
||||||
|
},
|
||||||
shape = RoundedCornerShape(14.dp),
|
shape = RoundedCornerShape(14.dp),
|
||||||
color = MulticaColors.Surface.copy(alpha = 0.82f),
|
color = MulticaColors.Surface.copy(alpha = 0.82f),
|
||||||
border = androidx.compose.foundation.BorderStroke(1.dp, MulticaColors.Border.copy(alpha = 0.76f)),
|
border = androidx.compose.foundation.BorderStroke(1.dp, MulticaColors.Border.copy(alpha = 0.76f)),
|
||||||
@@ -10844,8 +10861,9 @@ val replyActionModifier = Modifier.size(42.dp)
|
|||||||
.focusRequester(focusRequester)
|
.focusRequester(focusRequester)
|
||||||
.semantics(mergeDescendants = true) { contentDescription = "Issue Comment Reply Input $parentId" },
|
.semantics(mergeDescendants = true) { contentDescription = "Issue Comment Reply Input $parentId" },
|
||||||
label = if (zh) "写回复..." else "Write a reply...",
|
label = if (zh) "写回复..." else "Write a reply...",
|
||||||
minLines = 2,
|
minLines = if (replyFocused) 3 else 3,
|
||||||
maxLines = 5,
|
maxLines = if (replyFocused) 6 else 7,
|
||||||
|
onFocusedChange = { replyFocused = it },
|
||||||
)
|
)
|
||||||
if (pendingAttachments.isNotEmpty()) {
|
if (pendingAttachments.isNotEmpty()) {
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||||
@@ -11148,6 +11166,14 @@ private fun IssueCommentInputBar(
|
|||||||
) {
|
) {
|
||||||
var mentionPickerOpen by remember { mutableStateOf(false) }
|
var mentionPickerOpen by remember { mutableStateOf(false) }
|
||||||
var mentionQuery by remember { mutableStateOf("") }
|
var mentionQuery by remember { mutableStateOf("") }
|
||||||
|
var commentFocused by remember { mutableStateOf(false) }
|
||||||
|
val focusManager = LocalFocusManager.current
|
||||||
|
val keyboard = LocalSoftwareKeyboardController.current
|
||||||
|
val configuration = LocalConfiguration.current
|
||||||
|
val focusedComposerHeight = ChatExperiencePolicy.focusedComposerHeightPx(
|
||||||
|
ChatExperiencePolicy.Route.ISSUE_COMMENT,
|
||||||
|
configuration.screenHeightDp,
|
||||||
|
).dp
|
||||||
val mentionItems = remember(agents, members, squads, mentionQuery) {
|
val mentionItems = remember(agents, members, squads, mentionQuery) {
|
||||||
val query = mentionQuery.trim().lowercase()
|
val query = mentionQuery.trim().lowercase()
|
||||||
val agentItems = agents
|
val agentItems = agents
|
||||||
@@ -11208,19 +11234,44 @@ private fun IssueCommentInputBar(
|
|||||||
// Issue Comment Cupertino Composer mirrors Web comment-input.tsx's rounded editor surface.
|
// Issue Comment Cupertino Composer mirrors Web comment-input.tsx's rounded editor surface.
|
||||||
val composerActionModifier = Modifier.size(38.dp)
|
val composerActionModifier = Modifier.size(38.dp)
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.heightIn(min = if (commentFocused) focusedComposerHeight else 0.dp)
|
||||||
|
.semantics {
|
||||||
|
contentDescription = if (commentFocused) {
|
||||||
|
"Issue Comment Focused Composer Expanded"
|
||||||
|
} else {
|
||||||
|
"Issue Comment Compact Composer"
|
||||||
|
}
|
||||||
|
},
|
||||||
shape = RoundedCornerShape(14.dp),
|
shape = RoundedCornerShape(14.dp),
|
||||||
color = MulticaColors.Surface,
|
color = MulticaColors.Surface,
|
||||||
tonalElevation = 0.dp,
|
tonalElevation = 0.dp,
|
||||||
shadowElevation = 0.dp,
|
shadowElevation = 0.dp,
|
||||||
border = androidx.compose.foundation.BorderStroke(1.dp, MulticaColors.Border.copy(alpha = 0.86f)),
|
border = androidx.compose.foundation.BorderStroke(1.dp, MulticaColors.Border.copy(alpha = 0.86f)),
|
||||||
) {
|
) {
|
||||||
Row(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(start = 8.dp, top = 8.dp, end = 8.dp, bottom = 8.dp),
|
.padding(start = 8.dp, top = 8.dp, end = 8.dp, bottom = 8.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
MulticaTextField(
|
||||||
|
value = draft,
|
||||||
|
onValueChange = onDraftChange,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
label = "",
|
||||||
|
placeholder = if (zh) "评论" else "Comment",
|
||||||
|
showLabel = false,
|
||||||
|
contentDescription = "Issue Comment Input",
|
||||||
|
minLines = if (commentFocused) 3 else 1,
|
||||||
|
maxLines = if (commentFocused) 6 else 4,
|
||||||
|
onFocusedChange = { commentFocused = it },
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.spacedBy(7.dp),
|
horizontalArrangement = Arrangement.spacedBy(7.dp),
|
||||||
verticalAlignment = Alignment.Bottom,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
MulticaIconPillButton(
|
MulticaIconPillButton(
|
||||||
icon = Icons.Outlined.Image,
|
icon = Icons.Outlined.Image,
|
||||||
@@ -11245,17 +11296,22 @@ private fun IssueCommentInputBar(
|
|||||||
tone = if (mentionPickerOpen) MulticaButtonTone.Primary else MulticaButtonTone.Secondary,
|
tone = if (mentionPickerOpen) MulticaButtonTone.Primary else MulticaButtonTone.Secondary,
|
||||||
modifier = composerActionModifier,
|
modifier = composerActionModifier,
|
||||||
)
|
)
|
||||||
MulticaTextField(
|
if (commentFocused || mentionPickerOpen) {
|
||||||
value = draft,
|
MulticaIconPillButton(
|
||||||
onValueChange = onDraftChange,
|
icon = Icons.Outlined.Close,
|
||||||
modifier = Modifier.weight(1f),
|
contentDescription = "Issue Comment Cancel Input",
|
||||||
label = "",
|
onClick = {
|
||||||
placeholder = if (zh) "评论" else "Comment",
|
mentionPickerOpen = false
|
||||||
showLabel = false,
|
mentionQuery = ""
|
||||||
contentDescription = "Issue Comment Input",
|
commentFocused = false
|
||||||
minLines = 1,
|
focusManager.clearFocus(force = true)
|
||||||
maxLines = 4,
|
keyboard?.hide()
|
||||||
|
},
|
||||||
|
tone = MulticaButtonTone.Ghost,
|
||||||
|
modifier = composerActionModifier,
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
MulticaIconPillButton(
|
MulticaIconPillButton(
|
||||||
icon = Icons.AutoMirrored.Outlined.Send,
|
icon = Icons.AutoMirrored.Outlined.Send,
|
||||||
contentDescription = if (zh) "发送评论" else "Send Comment",
|
contentDescription = if (zh) "发送评论" else "Send Comment",
|
||||||
@@ -11275,6 +11331,7 @@ private fun IssueCommentInputBar(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (mentionPickerOpen) {
|
if (mentionPickerOpen) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ import com.airbnb.lottie.compose.rememberLottieComposition
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.SolidColor
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
@@ -1612,6 +1613,7 @@ fun MulticaCupertinoTextField(
|
|||||||
singleLine: Boolean = false,
|
singleLine: Boolean = false,
|
||||||
minLines: Int = 1,
|
minLines: Int = 1,
|
||||||
maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
|
maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
|
||||||
|
onFocusedChange: ((Boolean) -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
val fieldDescription = contentDescription ?: "Multica Cupertino Text Field $label"
|
val fieldDescription = contentDescription ?: "Multica Cupertino Text Field $label"
|
||||||
val centerSingleLineContent = singleLine || minLines <= 1
|
val centerSingleLineContent = singleLine || minLines <= 1
|
||||||
@@ -1651,7 +1653,8 @@ fun MulticaCupertinoTextField(
|
|||||||
onValueChange = onValueChange,
|
onValueChange = onValueChange,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.wrapContentHeight(),
|
.wrapContentHeight()
|
||||||
|
.onFocusChanged { onFocusedChange?.invoke(it.isFocused) },
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
singleLine = singleLine,
|
singleLine = singleLine,
|
||||||
minLines = minLines,
|
minLines = minLines,
|
||||||
@@ -1694,6 +1697,7 @@ fun MulticaTextField(
|
|||||||
singleLine: Boolean = false,
|
singleLine: Boolean = false,
|
||||||
minLines: Int = 1,
|
minLines: Int = 1,
|
||||||
maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
|
maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
|
||||||
|
onFocusedChange: ((Boolean) -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
MulticaCupertinoTextField(
|
MulticaCupertinoTextField(
|
||||||
value = value,
|
value = value,
|
||||||
@@ -1707,6 +1711,7 @@ fun MulticaTextField(
|
|||||||
singleLine = singleLine,
|
singleLine = singleLine,
|
||||||
minLines = minLines,
|
minLines = minLines,
|
||||||
maxLines = maxLines,
|
maxLines = maxLines,
|
||||||
|
onFocusedChange = onFocusedChange,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user