


























@@ -4,13 +4,17 @@ import ai.openclaw.app.MainViewModel
44import ai.openclaw.app.chat.ChatMessage
55import ai.openclaw.app.chat.ChatMessageContent
66import ai.openclaw.app.chat.ChatPendingToolCall
7+import ai.openclaw.app.chat.OutgoingAttachment
78import ai.openclaw.app.ui.design.ClawListItem
89import ai.openclaw.app.ui.design.ClawPanel
910import ai.openclaw.app.ui.design.ClawStatus
1011import ai.openclaw.app.ui.design.ClawStatusPill
1112import ai.openclaw.app.ui.design.ClawTheme
13+import androidx.activity.compose.rememberLauncherForActivityResult
14+import androidx.activity.result.contract.ActivityResultContracts
1215import androidx.compose.foundation.BorderStroke
1316import androidx.compose.foundation.background
17+import androidx.compose.foundation.horizontalScroll
1418import androidx.compose.foundation.layout.Arrangement
1519import androidx.compose.foundation.layout.Box
1620import androidx.compose.foundation.layout.Column
@@ -27,13 +31,15 @@ import androidx.compose.foundation.layout.width
2731import androidx.compose.foundation.lazy.LazyColumn
2832import androidx.compose.foundation.lazy.items
2933import androidx.compose.foundation.lazy.rememberLazyListState
34+import androidx.compose.foundation.rememberScrollState
3035import androidx.compose.foundation.shape.CircleShape
3136import androidx.compose.foundation.shape.RoundedCornerShape
3237import androidx.compose.foundation.text.BasicTextField
3338import androidx.compose.material.icons.Icons
3439import androidx.compose.material.icons.automirrored.filled.ArrowBack
3540import androidx.compose.material.icons.automirrored.filled.Send
3641import androidx.compose.material.icons.filled.AttachFile
42+import androidx.compose.material.icons.filled.Close
3743import androidx.compose.material.icons.filled.Mic
3844import androidx.compose.material.icons.filled.Refresh
3945import androidx.compose.material3.HorizontalDivider
@@ -44,6 +50,7 @@ import androidx.compose.runtime.Composable
4450import androidx.compose.runtime.LaunchedEffect
4551import androidx.compose.runtime.collectAsState
4652import androidx.compose.runtime.getValue
53+import androidx.compose.runtime.mutableStateListOf
4754import androidx.compose.runtime.mutableStateOf
4855import androidx.compose.runtime.remember
4956import androidx.compose.runtime.rememberCoroutineScope
@@ -53,12 +60,15 @@ import androidx.compose.ui.Alignment
5360import androidx.compose.ui.Modifier
5461import androidx.compose.ui.graphics.Color
5562import androidx.compose.ui.graphics.SolidColor
63+import androidx.compose.ui.platform.LocalContext
5664import androidx.compose.ui.text.font.FontWeight
5765import androidx.compose.ui.text.style.TextAlign
5866import androidx.compose.ui.text.style.TextOverflow
5967import androidx.compose.ui.unit.dp
6068import androidx.compose.ui.unit.sp
69+import kotlinx.coroutines.Dispatchers
6170import kotlinx.coroutines.launch
71+import kotlinx.coroutines.withContext
6272import java.text.DateFormat
6373import java.util.Date
6474import java.util.Locale
@@ -81,7 +91,27 @@ fun V2ChatScreen(
8191val sessions by viewModel.chatSessions.collectAsState()
8292val chatDraft by viewModel.chatDraft.collectAsState()
8393val pendingAssistantAutoSend by viewModel.pendingAssistantAutoSend.collectAsState()
94+val context = LocalContext.current
95+val resolver = context.contentResolver
8496val scope = rememberCoroutineScope()
97+val attachments = remember { mutableStateListOf<PendingImageAttachment>() }
98+val pickImages =
99+ rememberLauncherForActivityResult(ActivityResultContracts.GetMultipleContents()) { uris ->
100+if (uris.isNullOrEmpty()) return@rememberLauncherForActivityResult
101+ scope.launch(Dispatchers.IO) {
102+val next =
103+ uris.take(8).mapNotNull { uri ->
104+try {
105+ loadSizedImageAttachment(resolver, uri)
106+ } catch (_: Throwable) {
107+null
108+ }
109+ }
110+ withContext(Dispatchers.Main) {
111+ attachments.addAll(next)
112+ }
113+ }
114+ }
8511586116LaunchedEffect(Unit) {
87117 viewModel.loadChat(mainSessionKey)
@@ -146,18 +176,31 @@ fun V2ChatScreen(
146176V2ChatComposer(
147177 value = input,
148178 onValueChange = { input = it },
179+ attachments = attachments,
149180 thinkingLevel = thinkingLevel,
150181 healthOk = healthOk,
151182 pendingRunCount = pendingRunCount,
152183 onThinkingLevelChange = viewModel::setChatThinkingLevel,
184+ onPickImages = { pickImages.launch("image/*") },
185+ onRemoveAttachment = { id -> attachments.removeAll { it.id == id } },
153186 onVoice = onVoice,
154187 onAbort = viewModel::abortChat,
155188 onSend = {
156189val message = input.trim()
157-if (message.isEmpty()) return@V2ChatComposer
190+if (message.isEmpty() && attachments.isEmpty()) return@V2ChatComposer
191+val outgoing =
192+ attachments.map { attachment ->
193+OutgoingAttachment(
194+ type = "image",
195+ mimeType = attachment.mimeType,
196+ fileName = attachment.fileName,
197+ base64 = attachment.base64,
198+ )
199+ }
158200 input = ""
201+ attachments.clear()
159202 scope.launch {
160- viewModel.sendChat(message = message, thinking = thinkingLevel, attachments = emptyList())
203+ viewModel.sendChat(message = message, thinking = thinkingLevel, attachments = outgoing)
161204 }
162205 },
163206 )
@@ -537,21 +580,28 @@ private fun V2ChatNotice(
537580private fun V2ChatComposer(
538581value: String,
539582onValueChange: (String) -> Unit,
583+attachments: List<PendingImageAttachment>,
540584thinkingLevel: String,
541585healthOk: Boolean,
542586pendingRunCount: Int,
543587onThinkingLevelChange: (String) -> Unit,
588+onPickImages: () -> Unit,
589+onRemoveAttachment: (String) -> Unit,
544590onVoice: () -> Unit,
545591onAbort: () -> Unit,
546592onSend: () -> Unit,
547593) {
548594Column(modifier = Modifier.fillMaxWidth().imePadding(), verticalArrangement = Arrangement.spacedBy(4.dp)) {
595+if (attachments.isNotEmpty()) {
596+V2AttachmentStrip(attachments = attachments, onRemoveAttachment = onRemoveAttachment)
597+ }
598+549599V2ChatContextMeter(thinkingLevel = thinkingLevel, onClick = { onThinkingLevelChange(nextThinkingValue(thinkingLevel)) })
550600551601Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp)) {
552-V2ChatInputPill(value = value, onValueChange = onValueChange, onVoice = onVoice, modifier = Modifier.weight(1f))
602+V2ChatInputPill(value = value, onValueChange = onValueChange, onPickImages = onPickImages, onVoice = onVoice, modifier = Modifier.weight(1f))
553603V2SendButton(
554- enabled = healthOk && pendingRunCount == 0 && value.trim().isNotEmpty(),
604+ enabled = healthOk && pendingRunCount == 0 && (value.trim().isNotEmpty() || attachments.isNotEmpty()),
555605 onClick = onSend,
556606 )
557607 }
@@ -627,6 +677,7 @@ private fun V2ChatContextMeter(
627677private fun V2ChatInputPill(
628678value: String,
629679onValueChange: (String) -> Unit,
680+onPickImages: () -> Unit,
630681onVoice: () -> Unit,
631682modifier: Modifier = Modifier,
632683) {
@@ -642,7 +693,11 @@ private fun V2ChatInputPill(
642693 verticalAlignment = Alignment.CenterVertically,
643694 horizontalArrangement = Arrangement.spacedBy(7.dp),
644695 ) {
645-Icon(imageVector = Icons.Default.AttachFile, contentDescription = "Attach file", modifier = Modifier.size(13.dp), tint = ClawTheme.colors.text)
696+Surface(onClick = onPickImages, modifier = Modifier.size(ClawTheme.spacing.touchTarget), shape = CircleShape, color = ClawTheme.colors.surfaceRaised, contentColor = ClawTheme.colors.text) {
697+Box(contentAlignment = Alignment.Center) {
698+Icon(imageVector = Icons.Default.AttachFile, contentDescription = "Attach image", modifier = Modifier.size(16.dp))
699+ }
700+ }
646701Box(modifier = Modifier.weight(1f)) {
647702BasicTextField(
648703 value = value,
@@ -677,6 +732,44 @@ private fun V2ChatInputPill(
677732 }
678733}
679734735+@Composable
736+private fun V2AttachmentStrip(
737+attachments: List<PendingImageAttachment>,
738+onRemoveAttachment: (String) -> Unit,
739+) {
740+Row(modifier = Modifier.fillMaxWidth().horizontalScroll(rememberScrollState()), horizontalArrangement = Arrangement.spacedBy(6.dp)) {
741+ attachments.forEach { attachment ->
742+V2AttachmentChip(fileName = attachment.fileName, onRemove = { onRemoveAttachment(attachment.id) })
743+ }
744+ }
745+}
746+747+@Composable
748+private fun V2AttachmentChip(
749+fileName: String,
750+onRemove: () -> Unit,
751+) {
752+Surface(
753+ shape = RoundedCornerShape(ClawTheme.radii.pill),
754+ color = ClawTheme.colors.surfaceRaised,
755+ contentColor = ClawTheme.colors.text,
756+ border = BorderStroke(1.dp, ClawTheme.colors.border),
757+ ) {
758+Row(
759+ modifier = Modifier.padding(start = 9.dp, top = 5.dp, end = 5.dp, bottom = 5.dp),
760+ verticalAlignment = Alignment.CenterVertically,
761+ horizontalArrangement = Arrangement.spacedBy(6.dp),
762+ ) {
763+Text(text = fileName, style = ClawTheme.type.caption, color = ClawTheme.colors.textMuted, maxLines = 1, overflow = TextOverflow.Ellipsis)
764+Surface(onClick = onRemove, modifier = Modifier.size(22.dp), shape = CircleShape, color = ClawTheme.colors.canvas, contentColor = ClawTheme.colors.text) {
765+Box(contentAlignment = Alignment.Center) {
766+Icon(imageVector = Icons.Default.Close, contentDescription = "Remove attachment", modifier = Modifier.size(13.dp))
767+ }
768+ }
769+ }
770+ }
771+}
772+680773private fun currentSessionTitle(
681774sessionKey: String,
682775sessions: List<ai.openclaw.app.chat.ChatSessionEntry>,
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。