Skip to content

Commit 6500fd2

Browse files
authored
Merge pull request #98 from rymorale/center-extended-product-card
Miscellaneous UI fixes
2 parents f2b05ea + b99027a commit 6500fd2

14 files changed

Lines changed: 250 additions & 63 deletions

File tree

Documentation/style-guide.md

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,7 @@ Feature toggles and interaction configuration.
248248
| JSON Key | Type | Default | Description |
249249
|----------|------|---------|-------------|
250250
| `behavior.productCard.cardStyle` | string | `"actionButton"` | Product card layout. `"actionButton"` = image overlay with primary/secondary action buttons; `"productDetail"` = extended card with image, badge, name, subtitle, and price. |
251+
| `behavior.productCard.cardsAlignment` | string | `"center"` | Horizontal alignment of product cards within their display area. `"start"` = left-aligned; `"center"` = centered; `"end"` = right-aligned. |
251252

252253
### Input
253254

@@ -301,7 +302,6 @@ Feature toggles and interaction configuration.
301302
|----------|------|---------|-------------|
302303
| `behavior.promptSuggestions.itemMaxLines` | number | `1` | Max lines for suggestion chip text before ellipsis. |
303304
| `behavior.promptSuggestions.showHeader` | boolean | `false` | Show a "Suggestions" header label above the chips. Label text is configurable via `text["suggestions.header"]`. |
304-
| `behavior.promptSuggestions.alignToMessage` | boolean | `false` | Align the suggestion chips to the message bubble edges. When `false`, uses the default offset. |
305305

306306
> **Tip:** To hide the header subtitle, set `text["header.subtitle"]` to `""`. The subtitle is automatically hidden when its text is blank.
307307
@@ -315,7 +315,8 @@ Feature toggles and interaction configuration.
315315
"carouselStyle": "scroll"
316316
},
317317
"productCard": {
318-
"cardStyle": "productDetail"
318+
"cardStyle": "productDetail",
319+
"cardsAlignment": "center"
319320
},
320321
"input": {
321322
"enableVoiceInput": true,
@@ -347,8 +348,7 @@ Feature toggles and interaction configuration.
347348
},
348349
"promptSuggestions": {
349350
"itemMaxLines": 1,
350-
"showHeader": true,
351-
"alignToMessage": true
351+
"showHeader": true
352352
}
353353
}
354354
}
@@ -554,7 +554,7 @@ Icon and image asset configuration.
554554

555555
| JSON Key | Type | Default | Description |
556556
|----------|------|---------|-------------|
557-
| `assets.icons.company` | string | `""` | Company icon displayed to the left of agent text message bubbles. Accepts a remote URL (`http://` or `https://`) or a local asset name (without extension) resolved from the app's `assets/icons/` folder. Supported local formats: `.png`, `.webp`, `.jpg`, `.jpeg`. Leave empty to show no icon. |
557+
| `assets.icons.company` | string | `""` | Company icon displayed to the left of agent text message bubbles. When set, ALL agent response elements — message text, product cards, and prompt suggestion chips — are automatically aligned to the icon column (flush with the right edge of the icon). Accepts a remote URL (`http://` or `https://`) or a local asset name (without extension) resolved from the app's `assets/icons/` folder. Supported local formats: `.png`, `.webp`, `.jpg`, `.jpeg`. Leave empty to show no icon. |
558558

559559
### Example
560560

@@ -731,8 +731,8 @@ Used when `behavior.productCard.cardStyle` is `"productDetail"`.
731731
| `--message-border-radius` | `cssLayout.messageBorderRadius` | `Double` | `10.0` | Message bubble corner radius (dp) |
732732
| `--message-padding` | `cssLayout.messagePadding` | `List<Double>` | `[8, 16]` | Message content padding (dp) |
733733
| `--message-max-width` | `cssLayout.messageMaxWidth` | `Double?` | `null` | Max message width (dp or %) |
734-
| `--agent-icon-size` | `cssLayout.agentIconSize` | `Double?` | `39.0` | Size (dp) of the agent icon shown to the left of agent text messages |
735-
| `--agent-icon-spacing` | `cssLayout.agentIconSpacing` | `Double?` | `12.0` | Horizontal gap (dp) between the agent icon and the message card |
734+
| `--agent-icon-size` | `cssLayout.agentIconSize` | `Double?` | `39.0` | Size (dp) of the agent icon shown to the left of agent messages. Also determines the start offset applied to product cards and prompt suggestion chips so they align with the message text column. |
735+
| `--agent-icon-spacing` | `cssLayout.agentIconSpacing` | `Double?` | `12.0` | Horizontal gap (dp) between the agent icon and the message card. Also contributes to the start offset of product cards and prompt suggestion chips. |
736736

737737
### Layout - Chat
738738

@@ -1147,6 +1147,7 @@ This section documents which properties are fully implemented, partially impleme
11471147
| `behavior.multimodalCarousel.cardClickAction` | ⚠️ | Parsed but not implemented in carousel composables | - |
11481148
| `behavior.multimodalCarousel.carouselStyle` || Switches between paged (prev/next/dots) and continuous scroll | `ProductCarousel` |
11491149
| `behavior.productCard.cardStyle` || Switches between action-button cards and extended product-detail cards | `RecommendationCards`, `ProductCarousel` |
1150+
| `behavior.productCard.cardsAlignment` || Controls horizontal alignment of product cards (`"start"`, `"center"`, `"end"`) | `RecommendationCards` |
11501151
| `behavior.input.enableVoiceInput` || Controls mic button visibility | `InputActionButtons` |
11511152
| `behavior.input.sendButtonStyle` || `"default"` (paper airplane) or `"arrow"` (filled circle with upward arrow) | `SendButton` |
11521153
| `behavior.input.disableMultiline` || Restricts input to a single line when `true` | `ChatTextField` |
@@ -1221,7 +1222,7 @@ This section documents which properties are fully implemented, partially impleme
12211222

12221223
| Property | Status | Notes | Used In |
12231224
|----------|--------|-------|---------|
1224-
| `assets.icons.company` || Company icon displayed to the left of agent text message bubbles | `ChatMessageItem` (`RenderTextMessageWithIcon`) |
1225+
| `assets.icons.company` || Company icon displayed to the left of agent text message bubbles. When set, product cards and prompt suggestion chips are automatically offset to align with the agent text column. | `ChatMessageItem` (`RenderTextMessageWithIcon`, `RenderMixedMessage`) |
12251226

12261227
### Theme Tokens - Typography
12271228

@@ -1449,6 +1450,7 @@ When creating themes for the Android SDK, focus on these **actively used** prope
14491450
- `behavior.input.enableVoiceInput` - Show/hide microphone button
14501451
- `behavior.input.sendButtonStyle` - `"default"` (paper airplane) or `"arrow"` (filled circle with upward arrow)
14511452
- `behavior.productCard.cardStyle` - Use `"productDetail"` for extended product cards (image, badge, name, subtitle, price)
1453+
- `behavior.productCard.cardsAlignment` - Horizontal alignment of product cards: `"start"` (left), `"center"` (default), or `"end"` (right)
14521454
- `behavior.multimodalCarousel.carouselStyle` - Use `"paged"` for prev/next/dots or `"scroll"` for continuous scroll
14531455
- `behavior.welcomeCard.closeButtonAlignment` - Close button position (`"start"` or `"end"`)
14541456
- `behavior.welcomeCard.promptFullWidth` - Full-width cards vs compact pill chips

code/concierge/src/androidTest/kotlin/com/adobe/marketing/mobile/concierge/ui/components/messages/ChatMessageItemTest.kt

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,46 @@ class ChatMessageItemTest {
111111
.assertIsDisplayed()
112112
}
113113

114+
@Test
115+
fun chatMessageItem_mixedContentMessage_withCompanyIcon_displaysTextContent() {
116+
// RenderMixedMessage applies an icon-column start offset when assets.icons.company is set.
117+
// The text content must still be visible.
118+
val theme = ConciergeThemeData(
119+
config = ConciergeThemeConfig(),
120+
tokens = ConciergeThemeTokens(
121+
assets = ConciergeThemeAssets(
122+
icons = ConciergeIconAssets(company = "https://example.com/brand-icon.png")
123+
)
124+
)
125+
)
126+
127+
val multimodalElement = MultimodalElement(
128+
id = "element-1",
129+
alttext = "Product Image",
130+
url = "https://example.com/image.jpg"
131+
)
132+
133+
val message = ChatMessage(
134+
content = MessageContent.Mixed(
135+
text = "Here are your results:",
136+
multimodalElements = listOf(multimodalElement)
137+
),
138+
isFromUser = false,
139+
timestamp = System.currentTimeMillis()
140+
)
141+
142+
composeTestRule.setContent {
143+
ConciergeTheme(theme = theme) {
144+
CompositionLocalProvider(LocalImageProvider provides DefaultImageProvider()) {
145+
ChatMessageItem(message = message)
146+
}
147+
}
148+
}
149+
150+
composeTestRule.onNodeWithText("Here are your results:")
151+
.assertIsDisplayed()
152+
}
153+
114154
@Test
115155
fun chatMessageItem_displaysCitationsWhenPresent() {
116156
val citations = listOf(

code/concierge/src/androidTest/kotlin/com/adobe/marketing/mobile/concierge/ui/theme/ConciergeStylesTest.kt

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,44 @@ class ConciergeStylesTest {
155155
assertEquals(expectedUserShape, style!!.userMessageShape)
156156
}
157157

158+
@Test
159+
fun messageBubbleStyle_defaultAgentIconDimensions_usedWhenTokensAbsent() {
160+
var style: ConciergeStyles.MessageBubbleStyle? = null
161+
162+
composeTestRule.setContent {
163+
ConciergeTheme {
164+
style = ConciergeStyles.messageBubbleStyle
165+
}
166+
}
167+
168+
composeTestRule.waitForIdle()
169+
assertNotNull(style)
170+
assertEquals(39.dp, style!!.agentIconSize)
171+
assertEquals(12.dp, style!!.agentIconSpacing)
172+
}
173+
174+
@Test
175+
fun messageBubbleStyle_agentIconDimensions_readFromCssLayoutTokens() {
176+
var style: ConciergeStyles.MessageBubbleStyle? = null
177+
val themeData = ConciergeThemeData(
178+
config = ConciergeThemeConfig(),
179+
tokens = ConciergeThemeTokens(
180+
cssLayout = ConciergeLayout(agentIconSize = 48.0, agentIconSpacing = 16.0)
181+
)
182+
)
183+
184+
composeTestRule.setContent {
185+
ConciergeTheme(theme = themeData) {
186+
style = ConciergeStyles.messageBubbleStyle
187+
}
188+
}
189+
190+
composeTestRule.waitForIdle()
191+
assertNotNull(style)
192+
assertEquals(48.dp, style!!.agentIconSize)
193+
assertEquals(16.dp, style!!.agentIconSpacing)
194+
}
195+
158196
// -----------------------------------------------------------------------
159197
// thinkingAnimationStyle
160198
// -----------------------------------------------------------------------

code/concierge/src/main/kotlin/com/adobe/marketing/mobile/concierge/ui/chat/ConciergeChatViewModel.kt

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -682,7 +682,7 @@ class ConciergeChatViewModel : AndroidViewModel {
682682
)
683683
updateAssistantMessageContent(
684684
MessageContent.Text(parsedMessage.messageContent),
685-
parsedMessage.promptSuggestions,
685+
emptyList(),
686686
parsedMessage.sources,
687687
interactionId = if (hasCtas) null else parsedMessage.interactionId,
688688
sseComplete = true
@@ -697,7 +697,7 @@ class ConciergeChatViewModel : AndroidViewModel {
697697
)
698698
removeLastAssistantPlaceholder()
699699
}
700-
appendOrderedElementMessages(parsedMessage.orderedElements)
700+
appendOrderedElementMessages(parsedMessage.orderedElements, parsedMessage.promptSuggestions)
701701
} else {
702702
// Legacy path: text-only or mixed message
703703
val messageContent = if (parsedMessage.multimodalElements.isEmpty()) {
@@ -732,7 +732,10 @@ class ConciergeChatViewModel : AndroidViewModel {
732732
* All cards are batched into one Mixed message at the position of the first Card element.
733733
* Each CTA becomes its own CtaButton message.
734734
*/
735-
private fun appendOrderedElementMessages(orderedElements: List<ParsedMultimodalItem>) {
735+
private fun appendOrderedElementMessages(
736+
orderedElements: List<ParsedMultimodalItem>,
737+
promptSuggestions: List<String> = emptyList()
738+
) {
736739
val cardElements = orderedElements
737740
.filterIsInstance<ParsedMultimodalItem.Card>()
738741
.map { it.element }
@@ -756,7 +759,8 @@ class ConciergeChatViewModel : AndroidViewModel {
756759
content = MessageContent.Mixed(text = "", multimodalElements = cardElements),
757760
isFromUser = false,
758761
timestamp = System.currentTimeMillis(),
759-
sseComplete = true
762+
sseComplete = true,
763+
promptSuggestions = promptSuggestions
760764
)
761765
_messages.update { it + cardMessage }
762766
}

code/concierge/src/main/kotlin/com/adobe/marketing/mobile/concierge/ui/components/card/RecommendationCards.kt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,11 @@ import androidx.compose.animation.fadeOut
1919
import androidx.compose.foundation.layout.Column
2020
import androidx.compose.foundation.layout.fillMaxWidth
2121
import androidx.compose.runtime.Composable
22+
import androidx.compose.ui.Alignment
2223
import androidx.compose.ui.Modifier
2324
import com.adobe.marketing.mobile.concierge.ConciergeConstants
2425
import com.adobe.marketing.mobile.concierge.network.MultimodalElement
26+
import com.adobe.marketing.mobile.concierge.ui.theme.CardsAlignment
2527
import com.adobe.marketing.mobile.concierge.ui.theme.ConciergeTheme
2628
import com.adobe.marketing.mobile.concierge.ui.theme.ProductCardStyle
2729
import com.adobe.marketing.mobile.services.Log
@@ -55,8 +57,14 @@ internal fun RecommendationCards(
5557
enter = fadeIn(animationSpec = tween(durationMillis = 220)),
5658
exit = fadeOut(animationSpec = tween(durationMillis = 180))
5759
) {
60+
val horizontalAlignment = when (ConciergeTheme.behavior?.productCard?.cardsAlignment) {
61+
CardsAlignment.CENTER -> Alignment.CenterHorizontally
62+
CardsAlignment.END -> Alignment.End
63+
else -> Alignment.Start
64+
}
5865
Column(
59-
modifier = modifier.fillMaxWidth()
66+
modifier = modifier.fillMaxWidth(),
67+
horizontalAlignment = horizontalAlignment
6068
) {
6169
val cardStyle = ConciergeTheme.behavior?.productCard?.cardStyle ?: ProductCardStyle.ACTION_BUTTON
6270
val useExtendedProductCards = cardStyle == ProductCardStyle.PRODUCT_DETAIL

code/concierge/src/main/kotlin/com/adobe/marketing/mobile/concierge/ui/components/messages/ChatMessageItem.kt

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -288,11 +288,14 @@ private fun RenderTextMessageWithIcon(
288288
}
289289
}
290290

291-
BotMessageSuffix(
292-
message = message,
293-
onSuggestionClick = onSuggestionClick,
294-
onCtaButtonClick = onCtaButtonClick
295-
)
291+
Row(modifier = Modifier.fillMaxWidth()) {
292+
Spacer(modifier = Modifier.width(style.agentIconSize + style.agentIconSpacing))
293+
BotMessageSuffix(
294+
message = message,
295+
onSuggestionClick = onSuggestionClick,
296+
onCtaButtonClick = onCtaButtonClick
297+
)
298+
}
296299
}
297300
}
298301

@@ -309,28 +312,43 @@ private fun RenderMixedMessage(
309312
) {
310313
val style = ConciergeStyles.messageBubbleStyle
311314
val content = message.content as MessageContent.Mixed
315+
val companyIconName = if (!message.isFromUser) ConciergeTheme.tokens?.assets?.icons?.company?.takeIf { it.isNotEmpty() } else null
312316
val messageAlignment = ConciergeTheme.behavior?.chat?.messageAlignment ?: ChatMessageAlignment.START
313317

314318
Column(
315-
modifier = Modifier.fillMaxWidth()
319+
modifier = Modifier
320+
.fillMaxWidth()
321+
.then(
322+
if (companyIconName != null) Modifier.padding(start = style.agentIconSize + style.agentIconSpacing)
323+
else Modifier
324+
)
316325
) {
317326
Card(
318327
modifier = Modifier
319328
.then(messageAlignment.toModifier())
320-
.padding(style.padding),
329+
.padding(
330+
top = if (companyIconName != null) 0.dp else style.padding,
331+
bottom = style.padding,
332+
end = style.padding,
333+
start = if (companyIconName != null) 0.dp else style.padding
334+
),
321335
colors = CardDefaults.cardColors(
322336
containerColor = style.botMessageBackgroundColor
323337
),
324338
elevation = CardDefaults.cardElevation(defaultElevation = style.elevation),
325339
shape = style.shape
326340
) {
327341
Box(
328-
modifier = Modifier.padding(style.innerPadding)
342+
modifier = Modifier
343+
.fillMaxWidth()
344+
.padding(
345+
top = if (companyIconName != null) 0.dp else style.innerPadding,
346+
bottom = style.innerPadding,
347+
end = style.innerPadding,
348+
start = if (companyIconName != null) 0.dp else style.innerPadding
349+
)
329350
) {
330-
Column(
331-
modifier = Modifier
332-
.fillMaxWidth()
333-
) {
351+
Column(modifier = Modifier.fillMaxWidth()) {
334352
// Render text content if present
335353
if (content.text.isNotEmpty()) {
336354
ConciergeResponse(

code/concierge/src/main/kotlin/com/adobe/marketing/mobile/concierge/ui/components/welcomescreen/SuggestedPromptItem.kt

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@ import androidx.compose.foundation.layout.fillMaxWidth
2424
import androidx.compose.foundation.layout.padding
2525
import androidx.compose.foundation.layout.size
2626
import androidx.compose.foundation.layout.width
27-
import androidx.compose.material.icons.Icons
28-
import androidx.compose.material.icons.filled.AutoAwesome
2927
import androidx.compose.material3.Icon
28+
import androidx.compose.ui.res.painterResource
29+
import com.adobe.marketing.mobile.concierge.R
3030
import androidx.compose.material3.Surface
3131
import androidx.compose.material3.Text
3232
import androidx.compose.ui.text.style.TextOverflow
@@ -36,7 +36,9 @@ import androidx.compose.ui.Modifier
3636
import androidx.compose.ui.graphics.Color
3737
import androidx.compose.ui.graphics.painter.Painter
3838
import androidx.compose.ui.graphics.vector.ImageVector
39+
import androidx.compose.ui.graphics.vector.rememberVectorPainter
3940
import androidx.compose.ui.layout.ContentScale
41+
import androidx.compose.ui.unit.dp
4042
import com.adobe.marketing.mobile.concierge.ui.components.image.AsyncImage
4143
import com.adobe.marketing.mobile.concierge.ui.theme.ConciergeStyles
4244
import com.adobe.marketing.mobile.concierge.ui.theme.ConciergeTheme
@@ -98,12 +100,13 @@ internal fun SuggestedPromptItem(
98100
PromptImage(prompt = prompt, style = style)
99101
Spacer(modifier = Modifier.width(style.promptImageSpacing))
100102
} else {
103+
// TODO: Add style controls for the compact chip layout (icon size, spacing, etc.) if needed in the future
101104
// Compact chip layout: small icon only
102105
Icon(
103-
imageVector = prompt.imageVector ?: Icons.Default.AutoAwesome,
106+
painter = promptIconPainter(prompt.imageVector),
104107
contentDescription = null,
105108
tint = style.promptTextColor.copy(alpha = 0.6f),
106-
modifier = Modifier.size(style.promptImageSize)
109+
modifier = Modifier.size(16.dp)
107110
)
108111
Spacer(modifier = Modifier.width(style.promptImageSpacing))
109112
}
@@ -120,6 +123,10 @@ internal fun SuggestedPromptItem(
120123
}
121124
}
122125

126+
@Composable
127+
private fun promptIconPainter(imageVector: ImageVector?): Painter =
128+
imageVector?.let { rememberVectorPainter(it) } ?: painterResource(R.drawable.sparkle)
129+
123130
@Composable
124131
private fun PromptImage(
125132
prompt: SuggestedPrompt,
@@ -148,7 +155,7 @@ private fun PromptImage(
148155
contentScale = ContentScale.Crop
149156
)
150157
else -> Icon(
151-
imageVector = prompt.imageVector ?: Icons.Default.AutoAwesome,
158+
painter = promptIconPainter(prompt.imageVector),
152159
contentDescription = null,
153160
tint = style.promptTextColor.copy(alpha = 0.6f),
154161
modifier = Modifier.size(style.promptImageSize * 0.6f)

code/concierge/src/main/kotlin/com/adobe/marketing/mobile/concierge/ui/theme/ConciergeStyles.kt

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -652,13 +652,10 @@ internal object ConciergeStyles {
652652
val contentColor = themeColors.conciergeMessageText ?: themeColors.onSurface
653653
val textColor = themeColors.suggestionText ?: contentColor
654654
val behavior = ConciergeTheme.behavior?.promptSuggestions
655-
val bubbleStyle = messageBubbleStyle
656-
val alignToMessage = behavior?.alignToMessage ?: false
657-
val alignedPadding = bubbleStyle.padding + bubbleStyle.innerPadding
658655
return PromptSuggestionsStyle(
659656
containerTopPadding = 6.dp,
660-
containerStartPadding = if (alignToMessage) alignedPadding else 12.dp,
661-
containerEndPadding = if (alignToMessage) alignedPadding else 48.dp,
657+
containerStartPadding = 12.dp,
658+
containerEndPadding = 48.dp,
662659
itemSpacing = 8.dp,
663660
itemShape = RoundedCornerShape(ConciergeTheme.tokens?.cssLayout?.suggestionItemBorderRadius?.dp ?: 10.dp),
664661
itemBackgroundColor = themeColors.suggestionBackground ?: themeColors.container,

0 commit comments

Comments
 (0)