AI Assistant Multiple Documents: Compose HorizontalPager
AI chat for intelligent document analysis and interaction with multiple documents using HorizontalPager in Jetpack Compose. You must also run the AI Assistant demo server on your Machine.
/* * Copyright © 2025-2026 PSPDFKit GmbH. All rights reserved. * * The PSPDFKit Sample applications are licensed with a modified BSD license. * Please see License for details. This notice may not be removed from this file. */
package com.pspdfkit.catalog.examples.kotlin
import android.content.Contextimport android.content.Intentimport android.graphics.RectFimport android.os.Bundleimport androidx.activity.compose.setContentimport androidx.activity.enableEdgeToEdgeimport androidx.appcompat.app.AppCompatActivityimport androidx.compose.animation.AnimatedVisibilityimport androidx.compose.animation.expandVerticallyimport androidx.compose.animation.fadeInimport androidx.compose.animation.fadeOutimport androidx.compose.animation.shrinkVerticallyimport androidx.compose.animation.slideInVerticallyimport androidx.compose.animation.slideOutVerticallyimport androidx.compose.foundation.layout.Boximport androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.foundation.layout.fillMaxWidthimport androidx.compose.foundation.layout.navigationBarsPaddingimport androidx.compose.foundation.pager.HorizontalPagerimport androidx.compose.foundation.pager.rememberPagerStateimport androidx.compose.material3.ExperimentalMaterial3Apiimport androidx.compose.material3.Iconimport androidx.compose.material3.IconButtonimport androidx.compose.material3.MaterialThemeimport androidx.compose.material3.PrimaryTabRowimport androidx.compose.material3.Tabimport androidx.compose.material3.Textimport androidx.compose.material3.TopAppBarimport androidx.compose.material3.TopAppBarDefaultsimport androidx.compose.runtime.Composableimport androidx.compose.runtime.LaunchedEffectimport androidx.compose.runtime.collectAsStateimport androidx.compose.runtime.getValueimport androidx.compose.runtime.keyimport androidx.compose.runtime.mutableStateOfimport androidx.compose.runtime.rememberimport androidx.compose.runtime.rememberCoroutineScopeimport androidx.compose.runtime.setValueimport androidx.compose.ui.Alignmentimport androidx.compose.ui.Modifierimport androidx.compose.ui.graphics.Colorimport androidx.compose.ui.platform.LocalContextimport androidx.compose.ui.platform.LocalDensityimport androidx.compose.ui.res.painterResourceimport androidx.compose.ui.text.TextStyleimport androidx.compose.ui.text.font.FontWeightimport androidx.compose.ui.text.style.TextOverflowimport androidx.compose.ui.unit.dpimport androidx.compose.ui.unit.spimport com.pspdfkit.ai.createAiAssistantimport com.pspdfkit.ai.showAiAssistantimport com.pspdfkit.catalog.Rimport com.pspdfkit.catalog.SdkExampleimport com.pspdfkit.catalog.SdkExample.Companion.WELCOME_DOCimport com.pspdfkit.catalog.examples.kotlin.AiAssistantComposeActivity.Companion.PREFERENCES_NAMEimport com.pspdfkit.catalog.examples.kotlin.AiAssistantComposeActivity.Companion.PREF_AI_IP_ADDRESSimport com.pspdfkit.catalog.ui.theming.CatalogThemeimport com.pspdfkit.catalog.utils.JwtGeneratorimport com.pspdfkit.configuration.activity.PdfActivityConfigurationimport com.pspdfkit.configuration.page.PageScrollDirectionimport com.pspdfkit.document.providers.AssetDataProviderimport com.pspdfkit.document.providers.getDataProviderFromDocumentSourceimport com.pspdfkit.jetpack.compose.interactors.DefaultListenersimport com.pspdfkit.jetpack.compose.interactors.DocumentStateimport com.pspdfkit.jetpack.compose.interactors.getDefaultDocumentManagerimport com.pspdfkit.jetpack.compose.interactors.rememberDocumentStateimport com.pspdfkit.jetpack.compose.views.DocumentViewimport com.pspdfkit.ui.DocumentDescriptorimport io.nutrient.domain.ai.AiAssistantimport io.nutrient.domain.ai.AiAssistantProviderimport kotlinx.coroutines.flow.MutableStateFlowimport kotlinx.coroutines.flow.StateFlowimport kotlinx.coroutines.flow.asStateFlowimport kotlinx.coroutines.launch
/** * An example that demonstrates how to implement AI Assistant with multiple documents for the DocumentView in a Compose way. */class AiAssistantMultiDocComposeExample(context: Context) : SdkExample( context, R.string.jetpackAiAssistantMultiDocExampleTitle, R.string.jetpackAiAssistantMultiDocExampleDescription) { override fun launchExample(context: Context, configuration: PdfActivityConfiguration.Builder) { val intent = Intent(context, AiAssistantMultiDocComposeActivity::class.java) context.startActivity(intent) }}
class AiAssistantMultiDocComposeActivity : AppCompatActivity(), AiAssistantProvider { val assetFiles = listOf(WELCOME_DOC, "Scientific-paper.pdf", "Teacher.pdf", "The-Cosmic-Context-for-Life.pdf") val documentDescriptors = assetFiles.map { DocumentDescriptor.fromDataProviders(listOf(AssetDataProvider(it)), listOf(), listOf()) } private lateinit var activityConfiguration: PdfActivityConfiguration private val sessionId = AiAssistantMultiDocComposeActivity::class.java.simpleName private var ipAddressValue: String? = null var documentStateMap = mutableMapOf<Int, DocumentState>()
private val _currentDocumentIndex = MutableStateFlow(0) val currentDocumentIndex: StateFlow<Int> = _currentDocumentIndex.asStateFlow()
override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge() super.onCreate(savedInstanceState)
val preferences = getSharedPreferences(PREFERENCES_NAME, MODE_PRIVATE) ipAddressValue = preferences.getString(PREF_AI_IP_ADDRESS, "") ?: ""
setContent { var toolbarVisibility by remember { mutableStateOf(true) } activityConfiguration = PdfActivityConfiguration.Builder(LocalContext.current) .setAiAssistantEnabled(true) .defaultToolbarEnabled(false) .scrollDirection(PageScrollDirection.VERTICAL) .theme(R.style.PSPDFCatalog_AIAssistantDialog) .themeDark(R.style.PSPDFCatalog_AIAssistantDialog_Dark) .build()
CatalogTheme { val currentIndex by currentDocumentIndex.collectAsState()
Box( modifier = Modifier .fillMaxSize() .navigationBarsPadding() ) { val pagerState = rememberPagerState(currentIndex) { documentDescriptors.size } LaunchedEffect(currentIndex) { pagerState.animateScrollToPage(currentIndex) } HorizontalPager( state = pagerState, userScrollEnabled = false, beyondViewportPageCount = documentDescriptors.size, modifier = Modifier .fillMaxSize() ) { page -> if (page < documentDescriptors.size) { key(page) { val dataProvider = remember(page) { documentDescriptors[page].documentSource.getDataProviderFromDocumentSource() } // Create or retrieve the DocumentState for the current page. val documentState = documentStateMap[page] ?: rememberDocumentState(dataProvider, activityConfiguration).also { documentStateMap.put(page, it) } Box(modifier = Modifier.fillMaxSize()) { DocumentView( modifier = Modifier.fillMaxSize(), documentState = documentState, documentManager = getDefaultDocumentManager( uiListener = DefaultListeners.uiListeners( onImmersiveModeEnabled = { toolbarVisibility = !it } ) ) ) } } } }
CustomToolbar( onClick = { showAiAssistant(this@AiAssistantMultiDocComposeActivity) }, toolbarVisibility = toolbarVisibility, documentDescriptors = documentDescriptors, currentIndex ) { _currentDocumentIndex.value = it } } } } } var assistant: AiAssistant? = null
override fun getAiAssistant(): AiAssistant { return assistant ?: run { createAiAssistant( context = this@AiAssistantMultiDocComposeActivity, documentsDescriptors = documentDescriptors, serverUrl = "http://$ipAddressValue:4000", sessionId = sessionId, jwtToken = { documentIds -> JwtGenerator.generateJwtToken( this@AiAssistantMultiDocComposeActivity, claims = mapOf( "document_ids" to documentIds, "session_ids" to listOf(sessionId), "request_limit" to mapOf( "requests" to 160, "time_period_s" to 1000 * 60 * 10 ) ) ) } ).also { assistant = it } } }
override fun navigateTo( documentRect: List<RectF>, pageIndex: Int, documentIndex: Int ) { _currentDocumentIndex.value = documentIndex documentStateMap[documentIndex]?.documentConnection?.highlight(pageIndex, documentRect) }}
/** * A custom toolbar component for document viewer with animated visibility. * * @param onClick Callback for AI Assistant button click * @param toolbarVisibility Whether the toolbar should be visible * @param documentDescriptors List of [DocumentDescriptor] to display titles in the toolbar * @param currentIndex The index of the currently selected document * @param onPagerIndexChange Callback to change the pager index when a tab is clicked */@OptIn(ExperimentalMaterial3Api::class)@Composablefun CustomToolbar( onClick: () -> Unit, toolbarVisibility: Boolean, documentDescriptors: List<DocumentDescriptor>, currentIndex: Int, onPagerIndexChange: suspend (Int) -> Unit = {}) { val localDensity = LocalDensity.current val context = LocalContext.current val coroutineScope = rememberCoroutineScope()
// Animate toolbar appearance/disappearance with slide, expand and fade effects AnimatedVisibility( visible = toolbarVisibility, enter = slideInVertically { with(localDensity) { -40.dp.roundToPx() } } + expandVertically(expandFrom = Alignment.Top) + fadeIn(initialAlpha = 0.3f), exit = slideOutVertically() + shrinkVertically() + fadeOut() ) { Column { TopAppBar( title = { Column { Text( text = documentDescriptors[currentIndex].getTitle(context), color = Color.White, style = TextStyle( fontSize = 16.sp, fontWeight = FontWeight.Medium ) ) } }, actions = { IconButton( onClick = onClick ) { Icon( painter = painterResource(id = R.drawable.ic_ai_assistant), contentDescription = "AI Assistant", tint = Color.White ) } }, colors = TopAppBarDefaults.topAppBarColors( containerColor = MaterialTheme.colorScheme.primary, titleContentColor = MaterialTheme.colorScheme.onPrimary ) ) PrimaryTabRow( selectedTabIndex = currentIndex, modifier = Modifier.fillMaxWidth() ) { documentDescriptors.forEachIndexed { index, descriptor -> Tab( selected = currentIndex == index, onClick = { coroutineScope.launch { onPagerIndexChange.invoke(index) } }, text = { Text( text = descriptor.getTitle(context), maxLines = 1, overflow = TextOverflow.Ellipsis ) } ) } } } }}This code sample is an example that illustrates how to use our SDK. Please adapt it to your specific use case.