@@ -37,14 +37,26 @@ import ai.openclaw.app.ui.mobileCodeBorder
3737import ai.openclaw.app.ui.mobileCodeText
3838import ai.openclaw.app.ui.mobileText
3939import ai.openclaw.app.ui.mobileTextSecondary
40+ import androidx.compose.foundation.layout.size
41+ import androidx.compose.material.icons.Icons
42+ import androidx.compose.material.icons.filled.CheckCircle
43+ import androidx.compose.material.icons.filled.Info
44+ import androidx.compose.material.icons.filled.Warning
45+ import androidx.compose.material.icons.outlined.CheckCircle
46+ import androidx.compose.material.icons.outlined.Info
47+ import androidx.compose.material.icons.outlined.Warning
48+ import androidx.compose.material3.Icon
49+ import androidx.compose.ui.Alignment
50+ import androidx.compose.ui.graphics.Color
51+ import androidx.compose.ui.unit.sp
4052
4153private const val TAG = " AdaptiveCard"
4254
4355/* *
4456 * Renders an Adaptive Card from a parsed JSON map inline in a chat bubble.
4557 * Supports TextBlock, FactSet, ColumnSet, Container, Image (placeholder),
46- * Table, RichTextBlock, CodeBlock, ActionSet, ImageSet, Rating, ProgressBar ,
47- * Action.Submit, Action.Execute, and Action.OpenUrl.
58+ * Table, RichTextBlock, CodeBlock, ActionSet, Icon, List, ImageSet, Rating ,
59+ * ProgressBar, Action.Submit, Action.Execute, and Action.OpenUrl.
4860 * Unknown element types are silently skipped.
4961 */
5062@Composable
@@ -96,6 +108,8 @@ private fun RenderElement(element: Map<String, Any>) {
96108 " RichTextBlock" -> RenderRichTextBlock (element)
97109 " CodeBlock" -> RenderCodeBlock (element)
98110 " ActionSet" -> RenderActionSet (element)
111+ " Icon" -> RenderIcon (element)
112+ " List" -> RenderList (element)
99113 " ImageSet" -> RenderImageSet (element)
100114 " Rating" -> RenderRating (element)
101115 " ProgressBar" -> RenderProgressBar (element)
@@ -346,6 +360,85 @@ private fun RenderActionSet(element: Map<String, Any>) {
346360 }
347361}
348362
363+ @Composable
364+ private fun RenderIcon (element : Map <String , Any >) {
365+ val name = element[" name" ] as ? String ? : return
366+ val size = element[" size" ] as ? String
367+ val color = element[" color" ] as ? String
368+ val style = element[" style" ] as ? String // "regular" or "filled"
369+
370+ val iconSize = when (size?.lowercase()) {
371+ " small" -> 16 .dp
372+ " large" -> 32 .dp
373+ else -> 20 .dp // medium / default
374+ }
375+
376+ val iconColor = when (color?.lowercase()) {
377+ " accent" -> mobileAccent
378+ " good" , " green" -> Color (0xFF4CAF50 )
379+ " warning" , " yellow" -> Color (0xFFFFC107 )
380+ " attention" , " red" -> Color (0xFFF44336 )
381+ else -> mobileText
382+ }
383+
384+ // Map well-known icon names to Material icons; fall back to text label
385+ val isFilled = style?.lowercase() == " filled"
386+ val imageVector = when (name.lowercase()) {
387+ " info" , " information" -> if (isFilled) Icons .Filled .Info else Icons .Outlined .Info
388+ " warning" , " alert" -> if (isFilled) Icons .Filled .Warning else Icons .Outlined .Warning
389+ " checkmark" , " check" , " success" -> if (isFilled) Icons .Filled .CheckCircle else Icons .Outlined .CheckCircle
390+ else -> null
391+ }
392+
393+ if (imageVector != null ) {
394+ Icon (
395+ imageVector = imageVector,
396+ contentDescription = name,
397+ tint = iconColor,
398+ modifier = Modifier .size(iconSize),
399+ )
400+ } else {
401+ // Fallback: render icon name as a small styled label
402+ Text (
403+ text = " [$name ]" ,
404+ style = mobileCaption1,
405+ color = iconColor,
406+ )
407+ }
408+ }
409+
410+ @Composable
411+ private fun RenderList (element : Map <String , Any >) {
412+ val items = element.typedList(" items" )
413+ if (items.isEmpty()) return
414+
415+ val style = element[" style" ] as ? String // "ordered" or "unordered"
416+ val isOrdered = style?.lowercase() == " ordered"
417+
418+ Column (verticalArrangement = Arrangement .spacedBy(4 .dp)) {
419+ for ((index, item) in items.withIndex()) {
420+ val text = item[" text" ] as ? String ? : continue
421+ val icon = item.typedMap(" icon" )
422+ val prefix = if (isOrdered) " ${index + 1 } . " else " \u2022 "
423+
424+ Row (
425+ horizontalArrangement = Arrangement .spacedBy(6 .dp),
426+ verticalAlignment = Alignment .CenterVertically ,
427+ ) {
428+ if (icon != null ) {
429+ // Render inline icon before the list item text
430+ RenderIcon (icon)
431+ }
432+ Text (
433+ text = " $prefix$text " ,
434+ style = mobileCallout,
435+ color = mobileText,
436+ )
437+ }
438+ }
439+ }
440+ }
441+
349442@OptIn(ExperimentalLayoutApi ::class )
350443@Composable
351444private fun RenderImageSet (element : Map <String , Any >) {
@@ -471,3 +564,9 @@ private fun Map<String, Any>.typedList(key: String): List<Map<String, Any>> {
471564 ?.map { it as Map <String , Any > }
472565 ? : emptyList()
473566}
567+
568+ /* * Safely cast a nested map value from a loosely-typed map to Map<String, Any>?. */
569+ @Suppress(" UNCHECKED_CAST" )
570+ private fun Map <String , Any >.typedMap (key : String ): Map <String , Any >? {
571+ return this [key] as ? Map <String , Any >
572+ }
0 commit comments