Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions app/src/main/java/com/aryan/reader/AiSettingsScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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")
}
}
}

Expand Down Expand Up @@ -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
Expand Down
31 changes: 29 additions & 2 deletions app/src/main/java/com/aryan/reader/Common.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 = "",
Expand All @@ -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(
Expand Down Expand Up @@ -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()
Expand All @@ -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) {
Expand Down
28 changes: 27 additions & 1 deletion app/src/main/java/com/aryan/reader/pdf/PdfViewerScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -482,6 +484,7 @@ fun PdfViewerScreen(
}
}
var documentMetadataTitle by remember { mutableStateOf<String?>(null) }
var documentMetadataAuthor by remember { mutableStateOf<String?>(null) }
val activeLibraryItem by remember(uiState.allRecentFiles, uiState.rawLibraryFiles, uiState.recentFiles, uiState.selectedBookId, effectivePdfUri) {
derivedStateOf {
uiState.selectedBookId?.let { selectedId ->
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -3592,6 +3612,7 @@ fun PdfViewerScreen(
isDocumentReady = false
errorMessage = null
documentMetadataTitle = null
documentMetadataAuthor = null
isPrintBlockedForPasswordProtectedPdf = false
currentBookId = null
areAnnotationsLoaded = false
Expand Down Expand Up @@ -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} " +
Expand Down
Loading