feature/ai-assistant #2
@ -16,6 +16,8 @@ import com.ganjoor.android.data.Dictionary
|
||||
import com.ganjoor.android.data.Ganjoor
|
||||
import com.ganjoor.android.data.LocalBookmarks
|
||||
import com.ganjoor.android.ui.GanjoorApp
|
||||
import com.ganjoor.android.data.AssistantSettings
|
||||
import com.ganjoor.android.data.LocalAssistant
|
||||
import com.ganjoor.android.ui.LocalSettings
|
||||
import com.ganjoor.android.ui.Settings
|
||||
import com.ganjoor.android.ui.theme.GanjoorTheme
|
||||
@ -53,11 +55,13 @@ class MainActivity : ComponentActivity() {
|
||||
setContent {
|
||||
val settings = remember { settings }
|
||||
val bookmarks = remember { Bookmarks(applicationContext) }
|
||||
val assistant = remember { AssistantSettings(applicationContext) }
|
||||
// The client reads this flag on every request, so keep it in step with the setting.
|
||||
Ganjoor.offline = settings.value.offline
|
||||
CompositionLocalProvider(
|
||||
LocalSettings provides settings,
|
||||
LocalBookmarks provides bookmarks,
|
||||
LocalAssistant provides assistant,
|
||||
// The poetry is Persian, Urdu and Arabic throughout, so the whole app reads and
|
||||
// navigates right-to-left whichever UI language is selected.
|
||||
LocalLayoutDirection provides LayoutDirection.Rtl,
|
||||
|
||||
@ -16,7 +16,9 @@ import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
@ -46,6 +48,26 @@ fun AssistantResultSheet(prompt: String, text: String, onDismiss: () -> Unit) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A button that asks the reader's own assistant, or hands the question to another app when no
|
||||
* server is configured. Both paths are one tap and neither requires any setting up, which is the
|
||||
* point: the feature is optional, so it cannot assume anyone switched it on.
|
||||
*/
|
||||
@Composable
|
||||
fun AssistantAction(prompt: String, text: String, label: Int, instruction: Int) {
|
||||
val assistant = LocalAssistant.current
|
||||
val context = LocalContext.current
|
||||
val ask = stringResource(instruction)
|
||||
var open by remember { mutableStateOf(false) }
|
||||
|
||||
TextButton(onClick = {
|
||||
if (assistant.serverReady) open = true else context.shareText("$ask\n\n$text")
|
||||
}) {
|
||||
Text(stringResource(label))
|
||||
}
|
||||
if (open) AssistantResultSheet(prompt = prompt, text = text, onDismiss = { open = false })
|
||||
}
|
||||
|
||||
/** The same answer as a plain screen, for the selection-menu activity. */
|
||||
@Composable
|
||||
fun AssistantAnswer(text: String, prompt: String = "translate") {
|
||||
|
||||
@ -42,6 +42,9 @@ object DownloadsRoute
|
||||
@Serializable
|
||||
object AboutRoute
|
||||
|
||||
@Serializable
|
||||
object AssistantRoute
|
||||
|
||||
/**
|
||||
* Opens a page with the poet list as the only thing beneath it.
|
||||
*
|
||||
@ -75,6 +78,10 @@ fun GanjoorApp() {
|
||||
settingsOpen = false
|
||||
nav.navigate(AboutRoute)
|
||||
},
|
||||
LocalOpenAssistant provides {
|
||||
settingsOpen = false
|
||||
nav.navigate(AssistantRoute)
|
||||
},
|
||||
) {
|
||||
NavHost(
|
||||
navController = nav,
|
||||
@ -144,6 +151,9 @@ fun GanjoorApp() {
|
||||
composable<DownloadsRoute> {
|
||||
DownloadsScreen(onUp = { nav.goHome() })
|
||||
}
|
||||
composable<AssistantRoute> {
|
||||
AssistantScreen(onUp = { nav.navigateUp() }, onHome = { nav.goHome() })
|
||||
}
|
||||
composable<AboutRoute> {
|
||||
AboutScreen(onUp = { nav.navigateUp() }, onHome = { nav.goHome() })
|
||||
}
|
||||
|
||||
@ -387,6 +387,12 @@ private fun PassageActions(passage: Bookmark) {
|
||||
}) {
|
||||
Text(stringResource(R.string.share))
|
||||
}
|
||||
AssistantAction(
|
||||
prompt = "explain",
|
||||
text = passage.excerpt.orEmpty(),
|
||||
label = R.string.assistant_explain,
|
||||
instruction = R.string.assistant_ask_prompt,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -405,6 +411,12 @@ private fun PoemSummary(summary: String) {
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
AssistantAction(
|
||||
prompt = "summary",
|
||||
text = summary,
|
||||
label = R.string.assistant_translate,
|
||||
instruction = R.string.assistant_translate_prompt,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -44,6 +44,7 @@ val LocalOpenReadingSettings = staticCompositionLocalOf<() -> Unit> {
|
||||
|
||||
/** Opens the About and licences screen; the sheet is the only place that needs it. */
|
||||
val LocalOpenAbout = staticCompositionLocalOf<() -> Unit> { error("No about host") }
|
||||
val LocalOpenAssistant = staticCompositionLocalOf<() -> Unit> { error("No assistant host") }
|
||||
|
||||
/** Top-bar button that opens the reading settings sheet. */
|
||||
@Composable
|
||||
@ -137,11 +138,13 @@ fun ReadingSettingsSheet(onDismiss: () -> Unit) {
|
||||
settings.update { it.copy(showSummaries = on) }
|
||||
}
|
||||
|
||||
val openAssistant = LocalOpenAssistant.current
|
||||
TextButton(onClick = openAssistant, modifier = Modifier.padding(top = 16.dp)) {
|
||||
Text(stringResource(R.string.assistant_title))
|
||||
}
|
||||
|
||||
val openAbout = LocalOpenAbout.current
|
||||
TextButton(
|
||||
onClick = openAbout,
|
||||
modifier = Modifier.padding(top = 16.dp),
|
||||
) {
|
||||
TextButton(onClick = openAbout) {
|
||||
Text(stringResource(R.string.about))
|
||||
}
|
||||
}
|
||||
|
||||
@ -103,4 +103,6 @@
|
||||
<string name="selection_actions">چه کاری انجام شود؟</string>
|
||||
<string name="assistant_ask">پرسش از دستیار</string>
|
||||
<string name="assistant_ask_prompt">این شعر فارسی را ترجمه کن و معنایش را توضیح بده:</string>
|
||||
<string name="assistant_title">هوش مصنوعی</string>
|
||||
<string name="assistant_translate_prompt">این متن را ترجمه کن:</string>
|
||||
</resources>
|
||||
|
||||
@ -72,7 +72,7 @@
|
||||
<string name="oled_note">تاریک تھیمز پر لاگو ہوتا ہے؛ OLED اسکرین پر بیٹری بچاتا ہے</string>
|
||||
<string name="play_recitation">قرات سنیں</string>
|
||||
<string name="pause">وقفہ</string>
|
||||
<string name="assistant">اے آئی معاون</string>
|
||||
<string name="assistant">مصنوعی ذہانت کا معاون</string>
|
||||
<string name="assistant_intro">اختیاری ہے۔ ایپ کا ہر حصہ اس کے بغیر کام کرتا ہے۔ جب تک آپ خود ترتیب نہ دیں کچھ کہیں نہیں بھیجا جاتا، اور ایپ کے ساتھ کوئی اکاؤنٹ یا کلید نہیں آتی۔</string>
|
||||
<string name="assistant_mode">ماڈل تک رسائی کا طریقہ</string>
|
||||
<string name="assistant_off">بند</string>
|
||||
@ -103,4 +103,6 @@
|
||||
<string name="selection_actions">آپ کیا کرنا چاہیں گے؟</string>
|
||||
<string name="assistant_ask">اسسٹنٹ سے پوچھیں</string>
|
||||
<string name="assistant_ask_prompt">اس فارسی کلام کا ترجمہ کریں اور اس کا مطلب بیان کریں:</string>
|
||||
<string name="assistant_title">مصنوعی ذہانت</string>
|
||||
<string name="assistant_translate_prompt">اس کا ترجمہ کریں:</string>
|
||||
</resources>
|
||||
|
||||
@ -108,4 +108,6 @@
|
||||
<string name="selection_actions">What would you like to do?</string>
|
||||
<string name="assistant_ask">Ask an assistant</string>
|
||||
<string name="assistant_ask_prompt">Translate this Persian poetry and explain what it means:</string>
|
||||
<string name="assistant_title">AI assistant</string>
|
||||
<string name="assistant_translate_prompt">Translate this:</string>
|
||||
</resources>
|
||||
|
||||
25
app/src/test/java/com/ganjoor/android/ShareTest.kt
Normal file
25
app/src/test/java/com/ganjoor/android/ShareTest.kt
Normal file
@ -0,0 +1,25 @@
|
||||
package com.ganjoor.android
|
||||
|
||||
import com.ganjoor.android.ui.webUrl
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
class ShareTest {
|
||||
|
||||
@Test
|
||||
fun `builds a ganjoor link from a stored path`() {
|
||||
assertEquals("https://ganjoor.net/hafez/ghazal/sh1", webUrl("/hafez/ghazal/sh1"))
|
||||
}
|
||||
|
||||
/** Paths arrive with a leading slash and sometimes a trailing one; neither may double up. */
|
||||
@Test
|
||||
fun `trims slashes rather than doubling them`() {
|
||||
assertEquals("https://ganjoor.net/saadi/golestan", webUrl("/saadi/golestan/"))
|
||||
assertEquals("https://ganjoor.net/saadi/golestan", webUrl("saadi/golestan"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `leaves an absolute url alone`() {
|
||||
assertEquals("https://ganjoor.net/x", webUrl("https://ganjoor.net/x"))
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user