Skip to content

AI Writing Assistant Text Field

Overview

The AiWritingAssistantTextField is a unified composable that provides both standard and AI-enhanced text input capabilities. It automatically adapts its behavior based on whether an AI data provider is supplied:

  1. Plain Mode: When writingAssistDataProvider = null, renders a standard FioriNoteTextField without any AI features
  2. AI-Enhanced Mode: When writingAssistDataProvider is provided, adds a floating AI button and expandable bottom sheet intended for AI capabilities

Writing Assistant With Soft Keyboard Writing Assistant With Bottom Sheet

Key Features

  • Seamless Mode Switching: Pass null to use standard text field, or provide a data provider for AI features
  • Full FioriNoteTextField Support: All properties of FioriNoteTextField are exposed and available
  • Floating AI Entry Button: Appears above the keyboard when focused
  • Expandable Bottom Sheet: Displays AI actions and version controls
  • Version Management: Built-in undo/redo with version tracking
  • Feedback System: Thumbs up/down with detailed feedback collection
  • Prompt Group Pages: Support for categorized AI prompts
  • Customizable Content: Pass custom loading overlays and action layouts
  • Keyboard Aware: Proper IME handling for scrollable containers
  • Testable: Comprehensive test tags for UI automation

Quick Start

Using as Plain Text Field

Pass null for writingAssistDataProvider to use the component as a standard FioriNoteTextField:

var text by remember { mutableStateOf("") }

AiWritingAssistantTextField(
    value = text,
    onValueChange = { text = it },
    writingAssistDataProvider = null,  // No AI capabilities
    content = FioriTextFieldContent(
        label = "Enter your text",
        required = true
    )
)

Using with AI Capabilities

You enable AI features by providing the AiWritingAssistantDataProvider:

var text by remember { mutableStateOf("Initial text") }
val scope = rememberCoroutineScope()
val dataProvider = remember { MyAiDataProvider(text) }

// IMPORTANT: Initialize the data provider's state
dataProvider.RememberSavable()

AiWritingAssistantTextField(
    value = text,
    onValueChange = { text = it },
    writingAssistDataProvider = dataProvider,
    content = FioriTextFieldContent(label = "Goal Description"),
    writingAssistEntryButtonText = "Writing Assistant",
    aiActionsContent = { data ->
        Column(
            modifier = Modifier
                .clip(RoundedCornerShape(16.dp))
                .background(MaterialTheme.fioriHorizonAttributes.SapFioriColorBackground)
                .padding(16.dp)
        ) {
            AIActionButton(
                text = "Make it shorter",
                options = AIActionButtonOptions(
                    icon = FioriIcon(resId = R.drawable.ic_sap_icon_scissors),
                    enabled = !data.isLoading.value,
                    contentDescription = "Make Shorter"
                )
            ) {
                (data as MyAiDataProvider).makeItShort(scope)
            }

            AiActionDivider()

            AIActionButton(
                text = "Enhance writing",
                options = AIActionButtonOptions(
                    icon = FioriIcon(resId = R.drawable.ic_sap_icon_write),
                    enabled = !data.isLoading.value
                )
            ) {
                (data as MyAiDataProvider).enhanceWriting(scope)
            }
        }
    }
)

Parameters

AiWritingAssistantTextField exposes all parameters of FioriNoteTextField. It adds artificial intelligence-specific settings:

Core Parameters

Parameter Type Default Description
value String Required Current text value (ignored when writingAssistDataProvider is provided)
onValueChange (String) -> Unit Auto Callback when text changes. Default updates provider's state
modifier Modifier Modifier Modifier for styling the text field
writingAssistDataProvider AiWritingAssistantDataProvider? null Pass null for plain text field, or provide instance for AI features

Fiori Note Text Field Properties

All properties from FioriNoteTextField are fully supported:

Parameter Type Default Description
enabled Boolean true Whether the text field is enabled for interaction
readOnly Boolean false Whether the text field is read-only
textSelectable Boolean true Whether text can be selected when read-only
isError Boolean false Whether to show error state
content FioriTextFieldContent Default Configuration (label, placeholder, inline notice, error message, etc.)
trailingIcon FioriTextFieldIcon? Clear icon (AI mode) / null (plain) Trailing icon for the text field
textStyles FioriTextFieldTextStyles Default Text styles for field components
colors FioriTextFieldColors Default Color configuration for the text field
styles FioriTextFieldStyles Default Styles for the text field (min/max height, lines, etc.)
keyboardOptions KeyboardOptions Default Software keyboard options (keyboard type, IME action)
keyboardActions KeyboardActions Default Actions to handle IME events
visualTransformation VisualTransformation None Visual transformation (e.g., password masking)
interactionSource MutableInteractionSource Default Interaction source for tracking user interactions
onHyperlinkClick (() -> Unit)? null Callback for hyperlink click in inline notice
contentDescription String Auto Accessibility description

AI-Specific Parameters

Parameter Type Default Description
sheetConfig AiWritingAssistantSheetConfig Default Configuration for assistant sheet behavior (enabled, error states, feedback visibility)
writingAssistCallbacks AiWritingAssistantSheetCallbacks Default Runtime callbacks (thumbs up/down, feedback submission, navigation)
writingAssistEntryButtonText String "Writing Assistant" Label for the floating AI entry button
aiActionsContent @Composable (AiWritingAssistantDataProvider) -> Unit Empty Content for the assistant sheet (your AI action buttons)
defaultFeedbackChips MutableList<ChipData> Default Feedback options shown to users
sheetColors WritingAssistantSheetColors Default Color configuration for the sheet
sheetStyles WritingAssistantSheetStyles Default Style configuration for the sheet (max height, padding, etc.)
sheetTextStyles WritingAssistantSheetTextStyles Default Text style configuration for the sheet

Writing Assistant Data Provider

An abstract class that manages text revisions and state for AI writing assistance. You must call RememberSavable() in a @Composable context before using it.

Key Properties

Property Type Description
textFieldValue MutableState<String> Current text value
currentVersion MutableState<Int> Current revision version (1-based)
totalVersions MutableState<Int> Total number of revisions
isLoading MutableState<Boolean> Whether an AI operation is in progress
versionFeedback MutableState<List<FeedbackData>> Feedback data for each version

Key Methods

  • RememberSavable() - Required - Initializes state properties with proper state management
  • addRevision(text: String) - Adds new text revision
  • acceptCurrentText() - Accepts current revision and clears history
  • discardAllChanges() - Reverts to initial text
  • goPrevious() / goNext() - Navigate through revisions
  • enablePromptGroupPage(enable: Boolean, title: String = "") - Enable/disable categorized prompts
  • getCurrentText() - Get the current text value
  • setCurrentText(text: String) - Set current text without creating revision

Implementation Example

class MyAiWritingAssistant(
    initialText: String,
    private val aiService: AiService
) : AiWritingAssistantDataProvider(initialText) {

    fun makeItShort(scope: CoroutineScope) {
        isLoading.value = true  // Disables sheet interactions
        scope.launch {
            try {
                val shortened = aiService.shortenText(getCurrentText())
                addRevision(shortened)
            } catch (e: Exception) {
                // Handle error
            } finally {
                isLoading.value = false
            }
        }
    }

    fun enhanceWriting(scope: CoroutineScope) {
        isLoading.value = true
        scope.launch {
            try {
                val enhanced = aiService.enhanceText(getCurrentText())
                addRevision(enhanced)
            } finally {
                isLoading.value = false
            }
        }
    }
}

Configuration

Sheet Configuration

The AiWritingAssistantSheetConfig controls the assistant sheet’s behavior and appearance.

sheetConfig = AiWritingAssistantSheetConfig(
    enabled = true,
    isError = false,
    errorMessageHeadline = "Unable to generate content",
    errorActionText = "Retry",
    showInlineFeedback = true,
    thumbsEnabled = true,
    showFooter = true
)
Property Type Default Description
enabled Boolean true Whether interactive elements are enabled. Set to !data.isLoading.value to disable during requests
isError Boolean false Shows error state in the default page
isFeedbackError Boolean false [Deprecated] Error handling is now automatic via onFeedbackSubmitted callback
errorMessageHeadline String? null Error message headline
errorMessageDescription String? null Error message description
errorActionText String? null Error action button text
showInlineFeedback Boolean true Show inline thumbs up/down in the sheet
thumbsEnabled Boolean true Enable thumbs buttons for interaction
showFooter Boolean true Show the footer row (done/undo/redo buttons and version text)

Sheet Callbacks

Use AiWritingAssistantSheetCallbacks to handle user interactions:

writingAssistCallbacks = AiWritingAssistantSheetCallbacks(
    errorActionOnClick = { retryGeneration() },
    onThumbsUpClicked = { version ->
        analytics.trackThumbsUp(version)
    },
    onThumbsDownClicked = { version ->
        analytics.trackThumbsDown(version)
    },
    onFeedbackSubmitted = { version, feedbackData ->
        val deferred = CompletableDeferred<Unit>()
        scope.launch {
            try {
                // Simulate network delay
                delay(350)
                apiService.submitFeedback(feedbackData)
                deferred.complete(Unit)
            } catch (e: Exception) {
                // Error state will be shown automatically
                deferred.completeExceptionally(e)
            }
        }
        deferred
    },
    onVersionFeedbackChanged = { feedbackList ->
        // Sync feedback state externally if needed
        dataProvider.versionFeedback.value = feedbackList
    },
    onPromptGroupNavigateUp = {
        dataProvider.disablePromptGroupPage()
    }
)
Callback Parameters Description
errorActionOnClick () -> Unit Called when error action button is clicked
onThumbsUpClicked (version: Int) -> Unit Called when thumbs up is clicked
onThumbsDownClicked (version: Int) -> Unit Called when thumbs down is clicked
onFeedbackSubmitted (version: Int, feedbackData: FeedbackData) -> Job Returns Job for async submission. If Job completes exceptionally, error state shown automatically
onVersionFeedbackChanged (List<FeedbackData>) -> Unit Called when feedback list changes (for external state sync)
onPromptGroupNavigateUp () -> Unit Called when navigating back from prompt group page

Sheet Styles

You can customize the appearance of sheets with the WritingAssistantSheetStyles parameter:

sheetStyles = WritingAssistantSheetDefaults.styles(
    sheetMaxHeight = 450.dp,
    sheetMaxWidth = 640.dp,
    sheetCornerRadius = 28.dp,
    entryButtonPadding = 16.dp,
    entryButtonTopPadding = 12.dp,
    entryButtonBottomPadding = 12.dp,
    entryButtonHeight = 64.dp,
    feedbackTextToThumbsSpacing = 8.dp,
    thumbsButtonSpacing = 4.dp
)
Parameter Type Default Description
sheetMaxHeight Dp 460.dp Maximum height of the expanded sheet
sheetMaxWidth Dp 640.dp Maximum width (useful for tablets/landscape)
sheetCornerRadius Dp 28.dp Corner radius for the top corners
sheetPeakHeight Dp 56.dp Height when in peek state
sheetShadowElevation Dp 1.dp Shadow elevation
sheetTonalElevation Dp 1.dp Tonal elevation for Material 3
entryButtonPadding Dp 16.dp Horizontal padding for entry button
entryButtonTopPadding Dp 12.dp Top padding for entry button
entryButtonBottomPadding Dp 12.dp Bottom padding for entry button
entryButtonHeight Dp 64.dp Height of the entry button
feedbackTextToThumbsSpacing Dp 8.dp Spacing between feedback text and thumbs
thumbsButtonSpacing Dp 4.dp Spacing between thumbs up and down
footerStyles WritingAssistantSheetFooterStyles Default Styles for the footer section

Advanced Usage

Using AI Action Button

Use the AIActionButton to create action buttons in the assistant sheet. It provides consistent styling, accessibility, and icon management:

aiActionsContent = { data ->
    Column(
        modifier = Modifier
            .clip(RoundedCornerShape(16.dp))
            .background(MaterialTheme.fioriHorizonAttributes.SapFioriColorBackground)
            .padding(16.dp)
    ) {
        AIActionButton(
            text = "Enhance Writing",
            options = AIActionButtonOptions(
                icon = FioriIcon(resId = R.drawable.ic_sap_icon_write),
                enabled = !data.isLoading.value,
                contentDescription = "Enhance Writing",
                showIcon = true
            )
        ) {
            (data as MyAiDataProvider).enhanceWriting(scope)
        }

        AiActionDivider()

        AIActionButton(
            text = "Make Shorter",
            options = AIActionButtonOptions(
                icon = FioriIcon(resId = R.drawable.ic_sap_icon_scissors),
                enabled = !data.isLoading.value,
                contentDescription = "Make Shorter"
            )
        ) {
            (data as MyAiDataProvider).makeItShort(scope)
        }

        AiActionDivider()

        // Button without icon
        AIActionButton(
            text = "Other Options",
            options = AIActionButtonOptions(
                icon = FioriIcon(resId = R.drawable.ic_sap_icon_navigation_right_arrow),
                enabled = !data.isLoading.value,
                showIcon = false  // Hide the icon
            )
        ) {
            data.enablePromptGroupPage(title = "Other Options", enable = true)
        }
    }
}

AI Action Button Options Properties:

  • contentDescription: Accessibility label for screen readers
  • icon: FioriIcon for the button (defaults to AI icon)
  • enabled: Whether the button is clickable
  • showIcon: Whether to display the icon (default: true)

Disabling Sheet Content During Requests

To prevent user interactions while processing AI requests, set isLoading.value to true in your data provider. This disables all sheet interactions:

class MyAiDataProvider(initialText: String) : AiWritingAssistantDataProvider(initialText) {

    fun enhanceWriting(scope: CoroutineScope) {
        // Disable all sheet interactions
        isLoading.value = true

        scope.launch {
            try {
                val response = aiService.enhance(getCurrentText())
                addRevision(response)
            } catch (e: Exception) {
                // Handle error
            } finally {
                // Re-enable sheet interactions
                isLoading.value = false
            }
        }
    }
}

Then, in your sheet config, bind the enabled property:

sheetConfig = AiWritingAssistantSheetConfig(
    enabled = !data.isLoading.value,  // Disables during requests
    // ... other config
)

Custom Loading Overlays

Writing Assistant Custom Page

You can replace the default sheet content with custom loading indicators during request processing:

aiActionsContent = { data ->
    if ((data as MyAiDataProvider).showCustomLoadingOverlay.value) {
        // Show custom loading overlay
        Box(
            modifier = Modifier
                .fillMaxWidth()
                .background(Color.White)
                .padding(vertical = 48.dp),
            contentAlignment = Alignment.Center
        ) {
            Column(
                horizontalAlignment = Alignment.CenterHorizontally,
                verticalArrangement = Arrangement.Center
            ) {
                AnimatedListDrawables(color = MaterialTheme.fioriHorizonAttributes.SapFioriColorS2)
                Spacer(modifier = Modifier.height(16.dp))
                Text(
                    text = "Analyzing Text",
                    style = MaterialTheme.typography.bodyMedium
                )
            }
        }
    } else {
        // Show normal action buttons
        SheetActionsContent(data, scope)
    }
}

In your data provider:

class MyAiDataProvider(initialText: String) : AiWritingAssistantDataProvider(initialText) {
    var showCustomLoadingOverlay = mutableStateOf(false)

    fun customAction(scope: CoroutineScope) {
        showCustomLoadingOverlay.value = true
        isLoading.value = true

        scope.launch {
            delay(3.seconds)  // Simulate delay
            addRevision("Custom action result: " + getCurrentText())
            isLoading.value = false
            showCustomLoadingOverlay.value = false
        }
    }
}

Scrollable Container Support

When AiWritingAssistantTextField components are in scrollable containers, you must apply imePadding() before verticalScroll(). This ensures proper keyboard handling.

Column(
    modifier = Modifier
        .padding(paddingValues)
        .fillMaxSize()
        .imePadding()        // IMPORTANT: Must be BEFORE verticalScroll
        .verticalScroll(scrollState)
) {
    AiWritingAssistantTextField(
        writingAssistDataProvider = data,
        value = "Goal Description",
        // ... other parameters
    )

    // Other scrollable content
}

Important: The correct order is:

  1. imePadding() - Adjusts for keyboard
  2. verticalScroll() - Enables scrolling

This ensures the text field scrolls into view when the keyboard opens.

Prompt Group Pages

Support for categorized AI prompts with navigation:

AIActionButton(
    text = "Other Options",
    options = AIActionButtonOptions(
        icon = FioriIcon(resId = R.drawable.ic_sap_icon_navigation_right_arrow),
        enabled = !data.isLoading.value
    )
) {
    // Enable prompt group page with custom title
    data.enablePromptGroupPage(title = "More Options", enable = true)
}

// Handle back navigation
writingAssistCallbacks = AiWritingAssistantSheetCallbacks(
    onPromptGroupNavigateUp = {
        data.enablePromptGroupPage(enable = false)
    }
)

Control the visibility of the version-control footer (Done, Undo, and Redo buttons and version text):

sheetConfig = AiWritingAssistantSheetConfig(
    showFooter = false  // Hide the footer row
)

Use this when you show custom loading overlays. It provides a cleaner user interface during specific operations.

Writing Assistant Sheet

The sheet is used internally by AiWritingAssistantTextField and displays AI-generated content with version control and feedback.

Writing Assistant Anatomy

Sheet Features

  • Version control with undo/redo
  • Thumbs up/down feedback
  • Detailed feedback collection
  • Error state handling
  • Default page, feedback page, and prompt group page support

For detailed sheet configuration, see the Configuration section above.

Testing

The AI Writing Assistant provides comprehensive test tags for UI automation through AIWritingAssistantTestTags.

Enabling Test Mode

To test the entry button without a real keyboard, enable test mode:

@Test
fun testWritingAssistantButton() {
    composeTestRule.setContent {
        CompositionLocalProvider(LocalAIWritingAssistantTestMode provides true) {
            AiWritingAssistantTextField(
                writingAssistDataProvider = testDataProvider,
                value = "Test"
            )
        }
    }

    // Entry button will be visible after focusing
    composeTestRule.onNodeWithTag(AIWritingAssistantTestTags.TAG_ENTRY_BUTTON)
        .assertIsDisplayed()
}

Available Test Tags

Access test tags through AIWritingAssistantTestTags or via the composition local:

val tags = AIWritingAssistantTestTags.Builder().build()

// Text Field Components
composeTestRule.onNodeWithTag(tags.textFieldTag)  // Main text field
composeTestRule.onNodeWithTag(tags.entryButtonTag)  // Floating AI button

// Bottom Sheet Components
composeTestRule.onNodeWithTag(tags.sheetTag)  // Sheet container
composeTestRule.onNodeWithTag(tags.sheetContentTag)  // Content area
composeTestRule.onNodeWithTag(tags.topAppBarTag)  // Top app bar
composeTestRule.onNodeWithTag(tags.closeButtonTag)  // Close button

// Footer Controls
composeTestRule.onNodeWithTag(tags.footerTag)  // Footer container
composeTestRule.onNodeWithTag(tags.undoButtonTag)  // Undo button
composeTestRule.onNodeWithTag(tags.redoButtonTag)  // Redo button
composeTestRule.onNodeWithTag(tags.doneButtonTag)  // Done button
composeTestRule.onNodeWithTag(tags.versionTextTag)  // Version text

// Feedback Components
composeTestRule.onNodeWithTag(tags.thumbsUpButtonTag)  // Thumbs up
composeTestRule.onNodeWithTag(tags.thumbsDownButtonTag)  // Thumbs down
composeTestRule.onNodeWithTag(tags.feedbackTextTag)  // Feedback text

// Dialog
composeTestRule.onNodeWithTag(tags.alertDialogTag)  // Alert dialog
composeTestRule.onNodeWithTag(tags.alertDialogConfirmTag)  // Confirm button
composeTestRule.onNodeWithTag(tags.alertDialogDismissTag)  // Dismiss button

// Action Buttons (AIActionButton)
composeTestRule.onNodeWithTag(tags.actionButtonTag)  // Button container
composeTestRule.onNodeWithTag(tags.actionButtonLabelTag)  // Button text
composeTestRule.onNodeWithTag(tags.actionButtonIconTag)  // Button icon

Custom Test Tags

You can customize test tags with identifiers:

val customTags = AIWritingAssistantTestTags.Builder()
    .set(AIWritingAssistantTestTags::identifier, "my_test_case")
    .build()

CompositionLocalProvider(
    LocalAIWritingAssistantTestTagsManager provides AIWritingAssistantTestTagsManager(customTags)
) {
    AiWritingAssistantTextField(...)
}

// Now all tags will be prefixed with "my_test_case_"

Test Tag Constants

All default test tag values are available as constants:

AIWritingAssistantTestTags.TAG_TEXT_FIELD  // "ai_writing_assistant_text_field"
AIWritingAssistantTestTags.TAG_ENTRY_BUTTON  // "ai_writing_assistant_entry_button"
AIWritingAssistantTestTags.TAG_SHEET  // "ai_writing_assistant_sheet"
AIWritingAssistantTestTags.TAG_SHEET_CONTENT  // "ai_writing_assistant_sheet_content"
AIWritingAssistantTestTags.TAG_TOP_APP_BAR  // "ai_writing_assistant_top_app_bar"
AIWritingAssistantTestTags.TAG_CLOSE_BUTTON  // "ai_writing_assistant_close_button"
AIWritingAssistantTestTags.TAG_FOOTER  // "ai_writing_assistant_footer"
AIWritingAssistantTestTags.TAG_UNDO_BUTTON  // "ai_writing_assistant_undo_button"
AIWritingAssistantTestTags.TAG_REDO_BUTTON  // "ai_writing_assistant_redo_button"
AIWritingAssistantTestTags.TAG_DONE_BUTTON  // "ai_writing_assistant_done_button"
AIWritingAssistantTestTags.TAG_VERSION_TEXT  // "ai_writing_assistant_version_text"
AIWritingAssistantTestTags.TAG_THUMBS_UP_BUTTON  // "ai_writing_assistant_thumbs_up_button"
AIWritingAssistantTestTags.TAG_THUMBS_DOWN_BUTTON  // "ai_writing_assistant_thumbs_down_button"
AIWritingAssistantTestTags.TAG_FEEDBACK_TEXT  // "ai_writing_assistant_feedback_text"
AIWritingAssistantTestTags.TAG_ALERT_DIALOG  // "ai_writing_assistant_alert_dialog"
AIWritingAssistantTestTags.TAG_ALERT_DIALOG_CONFIRM  // "ai_writing_assistant_alert_dialog_confirm"
AIWritingAssistantTestTags.TAG_ALERT_DIALOG_DISMISS  // "ai_writing_assistant_alert_dialog_dismiss"
AIWritingAssistantTestTags.TAG_ACTION_BUTTON  // "ai_writing_assistant_action_button"
AIWritingAssistantTestTags.TAG_ACTION_BUTTON_LABEL  // "ai_writing_assistant_action_button_label"
AIWritingAssistantTestTags.TAG_ACTION_BUTTON_ICON  // "ai_writing_assistant_action_button_icon"

Example Test

@Test
fun testAIWritingAssistantFlow() {
    val dataProvider = TestAiDataProvider("Initial text")

    composeTestRule.setContent {
        CompositionLocalProvider(LocalAIWritingAssistantTestMode provides true) {
            AiWritingAssistantTextField(
                writingAssistDataProvider = dataProvider,
                value = ""
            )
        }
    }

    // Focus text field
    composeTestRule.onNodeWithTag(AIWritingAssistantTestTags.TAG_TEXT_FIELD)
        .performClick()

    // Click entry button
    composeTestRule.onNodeWithTag(AIWritingAssistantTestTags.TAG_ENTRY_BUTTON)
        .assertIsDisplayed()
        .performClick()

    // Verify sheet is shown
    composeTestRule.onNodeWithTag(AIWritingAssistantTestTags.TAG_SHEET)
        .assertIsDisplayed()

    // Click thumbs up
    composeTestRule.onNodeWithTag(AIWritingAssistantTestTags.TAG_THUMBS_UP_BUTTON)
        .performClick()

    // Click done
    composeTestRule.onNodeWithTag(AIWritingAssistantTestTags.TAG_DONE_BUTTON)
        .performClick()

    // Verify sheet is dismissed
    composeTestRule.onNodeWithTag(AIWritingAssistantTestTags.TAG_SHEET)
        .assertDoesNotExist()
}

API Reference

  • com.sap.cloud.mobile.fiori.compose.ai.aiwritingassistant.AiWritingAssistantTextField
  • com.sap.cloud.mobile.fiori.compose.ai.aiwritingassistant.AiWritingAssistantDataProvider
  • com.sap.cloud.mobile.fiori.compose.ai.aiwritingassistant.sheet.WritingAssistantSheet
  • com.sap.cloud.mobile.fiori.compose.ai.aiwritingassistant.sheet.AIActionButton
  • com.sap.cloud.mobile.fiori.compose.ai.aiwritingassistant.AIWritingAssistantTestTags

Last update: January 14, 2026