From 0f5852b018aaaeb23d1acec608e999ea06c5d7dc Mon Sep 17 00:00:00 2001 From: Sanju-Thomas Date: Thu, 2 Jul 2026 19:09:29 +0530 Subject: [PATCH] Add system prompt customizatio for AI summaries --- .../java/com/aryan/reader/AiSettingsScreen.kt | 72 +++++++++++++++++++ app/src/main/java/com/aryan/reader/Common.kt | 31 +++++++- .../com/aryan/reader/pdf/PdfViewerScreen.kt | 28 +++++++- 3 files changed, 128 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/aryan/reader/AiSettingsScreen.kt b/app/src/main/java/com/aryan/reader/AiSettingsScreen.kt index ecf9c619..61b380a1 100644 --- a/app/src/main/java/com/aryan/reader/AiSettingsScreen.kt +++ b/app/src/main/java/com/aryan/reader/AiSettingsScreen.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.rememberScrollState @@ -13,6 +14,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.Refresh import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.DropdownMenuItem @@ -58,6 +60,12 @@ fun AiSettingsScreen( "gemini" to stringResource(R.string.provider_gemini), "groq" to stringResource(R.string.provider_groq), ) + var promptText by remember { + mutableStateOf( + loadSummarizePrompt(context).ifBlank { DEFAULT_SUMMARIZE_PROMPT.trimIndent() } + ) + } + var showResetPromptConfirm by remember { mutableStateOf(false) } fun refresh() { settings = loadAiByokSettings(context) @@ -199,6 +207,52 @@ fun AiSettingsScreen( options = listOf(AiModelOption("gemini", GEMINI_CLOUD_TTS_MODEL)), onSelected = { updateModels(settings.copy(ttsModel = it)) } ) + + HorizontalDivider() + + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + "AI Page Explanation Prompt", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold + ) + Text( + "Customise the system instruction sent to the AI when explaining a book page. Leave as-is to use the built-in default.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + IconButton(onClick = { showResetPromptConfirm = true }) { + Icon( + Icons.Default.Refresh, + contentDescription = "Reset prompt to default" + ) + } + } + + OutlinedTextField( + value = promptText, + onValueChange = { promptText = it }, + modifier = Modifier + .fillMaxWidth() + .height(220.dp), + label = { Text("System Prompt") }, + maxLines = Int.MAX_VALUE + ) + + Button( + onClick = { + saveSummarizePrompt(context, promptText) + }, + modifier = Modifier.align(Alignment.End) + ) { + Text("Save Prompt") + } } } @@ -240,6 +294,24 @@ fun AiSettingsScreen( } ) } + + if (showResetPromptConfirm) { + AlertDialog( + onDismissRequest = { showResetPromptConfirm = false }, + title = { Text("Reset Prompt") }, + text = { Text("Restore the AI page explanation prompt to its built-in default? Your current edits will be lost.") }, + confirmButton = { + TextButton(onClick = { + resetSummarizePrompt(context) + promptText = DEFAULT_SUMMARIZE_PROMPT.trimIndent() + showResetPromptConfirm = false + }) { Text("Reset") } + }, + dismissButton = { + TextButton(onClick = { showResetPromptConfirm = false }) { Text(stringResource(R.string.action_cancel)) } + } + ) + } } @Composable diff --git a/app/src/main/java/com/aryan/reader/Common.kt b/app/src/main/java/com/aryan/reader/Common.kt index c42019ae..b94dd523 100644 --- a/app/src/main/java/com/aryan/reader/Common.kt +++ b/app/src/main/java/com/aryan/reader/Common.kt @@ -241,6 +241,7 @@ private const val PREF_AI_MODEL_SUMMARIZE = "model_summarize" private const val PREF_AI_MODEL_RECAP = "model_recap" private const val PREF_AI_TTS_MODEL = "tts_model" private const val PREF_AI_MODEL_EMPTY_MIGRATION_DONE = "model_empty_migration_done" +private const val PREF_AI_SUMMARIZE_PROMPT = "summarize_prompt" private const val AI_KEYSTORE_ALIAS = "reader_ai_byok_key_v1" private const val ENCRYPTION_PREFIX = "v1:" const val GEMINI_CLOUD_TTS_MODEL = "gemini-3.1-flash-live-preview" @@ -272,6 +273,17 @@ data class AiModelOption( val id: String = "$provider:$name" } +const val DEFAULT_SUMMARIZE_PROMPT = """ + System Role: You are an expert technical educator. You will be given an image of a page. Your task is to rewrite the provided page from a technical book into a clear, spoken-word explanation. You are not summarizing (don't skip details), and you are not translating literally (don't be robotic). You are explaining. + Task Instructions: + Preserve Technical Accuracy: Keep all specific terms, variables, and core logic. Do not omit the "meat" of the technical content. + Conversational Bridge: Use transition phrases like "Looking at the next section," "Essentially, this means," or "To put that into practice." This helps the listener follow along without seeing the page. + Handle Visuals/Code: If the text refers to a diagram or a code block, describe it briefly (e.g., "The code shows a loop where...") rather than reading out syntax like 'bracket, semicolon.' + Audio-First Formatting: + Write in full, flowing sentences. + Strictly No Markdown: No asterisks, bolding, or bullet points. + Use words for symbols if they are vital (e.g., say "plus" instead of "+").""" + data class AiByokSettings( val geminiKey: String = "", val groqKey: String = "", @@ -280,7 +292,8 @@ data class AiByokSettings( val defineModel: String = "", val summarizeModel: String = "", val recapModel: String = "", - val ttsModel: String = "" + val ttsModel: String = "", + val summarizePrompt: String = "" ) val aiByokModelOptions = listOf( @@ -366,7 +379,8 @@ fun loadAiByokSettings(context: Context): AiByokSettings { defineModel = prefs.getString(PREF_AI_MODEL_DEFINE, "") ?: "", summarizeModel = prefs.getString(PREF_AI_MODEL_SUMMARIZE, "") ?: "", recapModel = prefs.getString(PREF_AI_MODEL_RECAP, "") ?: "", - ttsModel = prefs.getString(PREF_AI_TTS_MODEL, "") ?: "" + ttsModel = prefs.getString(PREF_AI_TTS_MODEL, "") ?: "", + summarizePrompt = prefs.getString(PREF_AI_SUMMARIZE_PROMPT, "") ?: "" ) val geminiStored = prefs.getString(PREF_AI_GEMINI_KEY, "").orEmpty() val groqStored = prefs.getString(PREF_AI_GROQ_KEY, "").orEmpty() @@ -388,9 +402,22 @@ fun saveAiByokSettings(context: Context, settings: AiByokSettings) { putString(PREF_AI_MODEL_SUMMARIZE, settings.summarizeModel) putString(PREF_AI_MODEL_RECAP, settings.recapModel) putString(PREF_AI_TTS_MODEL, settings.ttsModel) + putString(PREF_AI_SUMMARIZE_PROMPT, settings.summarizePrompt) } } +fun loadSummarizePrompt(context: Context): String { + return context.aiPrefs().getString(PREF_AI_SUMMARIZE_PROMPT, "").orEmpty() +} + +fun saveSummarizePrompt(context: Context, prompt: String) { + context.aiPrefs().edit { putString(PREF_AI_SUMMARIZE_PROMPT, prompt) } +} + +fun resetSummarizePrompt(context: Context) { + context.aiPrefs().edit { putString(PREF_AI_SUMMARIZE_PROMPT, "") } +} + fun saveAiByokKey(context: Context, provider: String, key: String) { val current = loadAiByokSettings(context) val updated = when (provider) { diff --git a/app/src/main/java/com/aryan/reader/pdf/PdfViewerScreen.kt b/app/src/main/java/com/aryan/reader/pdf/PdfViewerScreen.kt index 25631cc2..190025c8 100644 --- a/app/src/main/java/com/aryan/reader/pdf/PdfViewerScreen.kt +++ b/app/src/main/java/com/aryan/reader/pdf/PdfViewerScreen.kt @@ -233,6 +233,8 @@ import com.aryan.reader.TtsSettingsSheet import com.aryan.reader.TtsWordReplacementsSheet import com.aryan.reader.areReaderAiFeaturesEnabled import com.aryan.reader.callByokGeminiInlineAi +import com.aryan.reader.DEFAULT_SUMMARIZE_PROMPT +import com.aryan.reader.loadSummarizePrompt import com.aryan.reader.cardTitle import com.aryan.reader.epubreader.AutoScrollControls import com.aryan.reader.epubreader.DictionarySettingsDialog @@ -482,6 +484,7 @@ fun PdfViewerScreen( } } var documentMetadataTitle by remember { mutableStateOf(null) } + var documentMetadataAuthor by remember { mutableStateOf(null) } val activeLibraryItem by remember(uiState.allRecentFiles, uiState.rawLibraryFiles, uiState.recentFiles, uiState.selectedBookId, effectivePdfUri) { derivedStateOf { uiState.selectedBookId?.let { selectedId -> @@ -3073,12 +3076,29 @@ fun PdfViewerScreen( @Suppress("KotlinConstantConditions") if (BuildConfig.FLAVOR == "oss") { val fullText = StringBuilder() + val bookName = documentMetadataTitle ?: originalFileName + val authorName = documentMetadataAuthor ?: uiState.recentFiles.find { it.uriString == effectivePdfUri.toString() }?.author ?: "Unknown Author" + val displayPageNumber = currentPage + 1 + + val userPromptBase = loadSummarizePrompt(context) + .ifBlank { DEFAULT_SUMMARIZE_PROMPT.trimIndent() } + val systemInstruction = """ + $userPromptBase + Output Structure: + "On page $displayPageNumber, the author explains [Insert the adapted technical explanation here]." + [Data Delimiter] + Below is the book details from which image is took. Please process this text according to the instructions above: + Book name: $bookName + Author: $authorName + page number: $displayPageNumber + """.trimIndent() + callByokGeminiInlineAi( context = context, feature = AiFeature.SUMMARIZE, mimeType = "image/jpeg", base64Data = base64Image, - systemInstruction = "You are an expert in analyzing visual content. You will be given an image of a page. Describe what is happening, identify key information, and summarize the text. Do not add a preamble.", + systemInstruction = systemInstruction, temperature = 0.2, maxTokens = 8192, onUpdate = { @@ -3592,6 +3612,7 @@ fun PdfViewerScreen( isDocumentReady = false errorMessage = null documentMetadataTitle = null + documentMetadataAuthor = null isPrintBlockedForPasswordProtectedPdf = false currentBookId = null areAnnotationsLoaded = false @@ -3730,6 +3751,11 @@ fun PdfViewerScreen( wrapper.pdfDocument.getDocumentMeta().title?.takeIf { it.isNotBlank() } } } + documentMetadataAuthor = (doc as? PdfDocumentWrapper)?.let { wrapper -> + PdfiumEngineProvider.withPdfium { + wrapper.pdfDocument.getDocumentMeta().author?.takeIf { it.isNotBlank() } + } + } Timber.tag(PDF_RENAME_TRACE_TAG).i( "pdfScreen.openEffect.metadataLoaded bookId=$currentBookId uri=$effectivePdfUri " + "documentMetadataTitle=$documentMetadataTitle activeCustomName=${activeLibraryItem?.customName} " +