Compare commits

..

3 Commits

8 changed files with 129 additions and 59 deletions

View File

@@ -109,10 +109,19 @@ class ExampleSentenceRequest(
override val requiredFields = listOf("word", "sourceSentence", "targetSentence")
init {
promptBuilder.basePrompt = "Provide one short, simple and clear example sentence for the word '$word' in $languageFirst and fully translate the sentence to $languageSecond, using $wordTranslation as a translation."
addDetail("Structure: { 'word': string, 'sourceSentence': string, 'targetSentence': string }.")
addDetail("Only include the fields above. Keep sentences concise and clear. Do not include any explanations or additional text.")
withJsonResponse("a JSON object with 'word' (the original word), 'sourceSentence' (the example sentence in the source language), and 'targetSentence' (the translation in the target language). Ensure all values are properly quoted strings.")
promptBuilder.basePrompt = """
Task: Create a short, concise natural example sentence in $languageFirst for the word '$word'.
Rules:
1. The 'sourceSentence' must be entirely in $languageFirst.
2. Do NOT use the word '$wordTranslation' in the 'sourceSentence'.
3. The 'targetSentence' must be the $languageSecond translation of the 'sourceSentence'.
4. In the 'targetSentence', use the word '$wordTranslation'.
""".trimIndent()
addDetail("Constraint: Ensure 'sourceSentence' contains ONLY $languageFirst and 'targetSentence' contains ONLY $languageSecond.")
addDetail("Structure: { 'word': '$word', 'sourceSentence': string, 'targetSentence': string }.")
withJsonResponse("a JSON object. Ensure no mixed-language sentences occur.")
}
}

View File

@@ -90,14 +90,14 @@ class DictionaryService(context: Context) {
@OptIn(ExperimentalTime::class)
suspend fun searchDefinition(query: String, language: Language): Result<DictionaryEntry> = withContext(Dispatchers.IO) {
try {
Log.i("DictionaryService", "Searching definition for word: $query in language: ${language.name}")
Log.i("DictionaryService", "Searching definition for word: $query in language: ${language.englishName}")
val requestedParts = getActivatedDictionaryOptions()
Log.d("DictionaryService", "Requested dictionary parts: $requestedParts")
val template = DictionaryDefinitionRequest(
word = query,
language = language.name,
language = language.englishName,
requestedParts = requestedParts
)
@@ -110,7 +110,7 @@ class DictionaryService(context: Context) {
word = apiResponse.word,
definition = apiResponse.parts,
languageCode = language.nameResId,
languageName = language.name,
languageName = language.englishName,
createdAt = Clock.System.now()
)
}.onFailure { exception ->
@@ -127,13 +127,13 @@ class DictionaryService(context: Context) {
*/
suspend fun getExampleSentence(word: String, wordTranslation: String, languageFirst: Language, languageSecond: Language): Result<Pair<String, String>> = withContext(Dispatchers.IO) {
try {
Log.i("DictionaryService", "Getting example sentence for word: $word (${languageFirst.name} -> ${languageSecond.name})")
Log.i("DictionaryService", "Getting example sentence for word: $word (${languageFirst.englishName} -> ${languageSecond.englishName})")
val template = ExampleSentenceRequest(
word = word,
wordTranslation = wordTranslation,
languageFirst = languageFirst.name,
languageSecond = languageSecond.name
languageFirst = languageFirst.englishName,
languageSecond = languageSecond.englishName
)
val result = apiRequestHandler.executeRequest(template)
@@ -179,7 +179,7 @@ class DictionaryService(context: Context) {
}
}
Log.i("DictionaryService", "Generating new word of the day for: $todayString in language: ${language.name}")
Log.i("DictionaryService", "Generating new word of the day for: $todayString in language: ${language.englishName}")
val topics = listOf(
"science", "literature", "history", "technology", "nature",
@@ -192,7 +192,7 @@ class DictionaryService(context: Context) {
Log.d("DictionaryService", "Selected topic for word of the day: $randomTopic")
val template = WordOfTheDayRequest(
language = language.name,
language = language.englishName,
category = randomTopic
)
@@ -205,7 +205,7 @@ class DictionaryService(context: Context) {
word = apiResponse.word,
definition = apiResponse.parts,
languageCode = language.nameResId,
languageName = language.name,
languageName = language.englishName,
createdAt = today
)
dictionaryRepository.saveWordOfTheDay(newEntry)
@@ -225,7 +225,7 @@ class DictionaryService(context: Context) {
suspend fun getEtymology(query: String, language: Language): Result<EtymologyData> = withContext(Dispatchers.IO) {
try {
Log.i("DictionaryService", "Getting etymology for word: $query in language: ${language.name}")
Log.i("DictionaryService", "Getting etymology for word: $query in language: ${language.englishName}")
val template = EtymologyRequest(
word = query,

View File

@@ -1,6 +1,7 @@
package eu.gaudian.translator.view.composable
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.spring
@@ -55,7 +56,8 @@ data class FabMenuItem(
fun AppFabMenu(
items: List<FabMenuItem>,
modifier: Modifier = Modifier,
title: String? = null
title: String? = null,
showFabText: Boolean = true
) {
var isMenuExpanded by remember { mutableStateOf(false) }
@@ -111,14 +113,16 @@ fun AppFabMenu(
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.padding(horizontal = 16.dp)
modifier = Modifier
.padding(horizontal = 16.dp)
.animateContentSize()
) {
Icon(
imageVector = AppIcons.Add,
contentDescription = stringResource(R.string.cd_toggle_menu),
modifier = Modifier.rotate(iconRotationAngle)
)
if (title != null) {
if (title != null && showFabText) {
Text(
text = title,
style = MaterialTheme.typography.labelLarge

View File

@@ -22,6 +22,7 @@ import eu.gaudian.translator.viewmodel.VocabularyViewModel
@Composable
fun VocabularyMenu(
modifier: Modifier = Modifier,
showFabText : Boolean = true
) {
val activity = LocalContext.current.findActivity()
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
@@ -48,7 +49,7 @@ fun VocabularyMenu(
)
)
AppFabMenu(items = menuItems, modifier = modifier, title = stringResource(R.string.label_add_vocabulary))
AppFabMenu(items = menuItems, modifier = modifier, title = stringResource(R.string.label_add_vocabulary), showFabText = showFabText)
if (showAddVocabularyDialog) {
AddVocabularyDialog(

View File

@@ -88,6 +88,7 @@ fun DashboardContent(
onNavigateToCategoryDetail: (Int) -> Unit,
onNavigateToCategoryList: () -> Unit,
onShowWordPairExerciseDialog: () -> Unit,
onScroll: (Boolean) -> Unit = {},
) {
val activity = LocalContext.current.findActivity()
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
@@ -167,6 +168,11 @@ fun DashboardContent(
lazyListState.firstVisibleItemScrollOffset
)
}
// Detect scroll and notify parent
LaunchedEffect(lazyListState.isScrollInProgress) {
onScroll(lazyListState.isScrollInProgress)
}
DisposableEffect(Unit) {
onDispose {
settingsViewModel.saveDashboardScrollState(

View File

@@ -4,6 +4,7 @@ package eu.gaudian.translator.view.vocabulary
import androidx.activity.ComponentActivity
import androidx.activity.compose.LocalActivity
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -112,6 +113,9 @@ fun MainVocabularyScreen(
var wpTrainingMode by remember { mutableStateOf(false) }
var wpDueTodayOnly by remember { mutableStateOf(false) }
var isScrolling by remember { mutableStateOf(false) }
if (showCustomExerciseDialog) {
StartExerciseDialog(
onDismiss = { showCustomExerciseDialog = false },
@@ -293,6 +297,8 @@ fun MainVocabularyScreen(
VocabularyTab.entries.find { it.route == currentRoute } ?: VocabularyTab.Dashboard
}
val showFabText = selectedTab == VocabularyTab.Dashboard && !isScrolling
val repoEmpty =
vocabularyViewModel.vocabularyItems.collectAsState(initial = emptyList()).value.isEmpty()
@@ -333,7 +339,8 @@ fun MainVocabularyScreen(
onNavigateToCategoryList = {
navController.navigate("category_list_screen")
},
onShowWordPairExerciseDialog = { showWordPairExerciseDialog = true }
onShowWordPairExerciseDialog = { showWordPairExerciseDialog = true },
onScroll = { isScrolling = it }
)
}
composable(VocabularyTab.Statistics.route) {
@@ -390,7 +397,7 @@ fun MainVocabularyScreen(
.padding(16.dp),
horizontalAlignment = Alignment.End
) {
VocabularyMenu(modifier = Modifier.onSizeChanged { menuHeightPx = it.height })
VocabularyMenu(modifier = Modifier.onSizeChanged { menuHeightPx = it.height }, showFabText = showFabText)
}
// Place the FAB separately and animate its bottom padding based on the menu height
@@ -405,16 +412,19 @@ fun MainVocabularyScreen(
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.padding(horizontal = 16.dp)
modifier = Modifier
.padding(horizontal = 16.dp)
.animateContentSize()
) {
Icon(
imageVector = AppIcons.Quiz,
contentDescription = null
)
if(showFabText) {
Text(
text = stringResource(R.string.label_start_exercise),
style = MaterialTheme.typography.labelLarge
)
)}
}
}
}

View File

@@ -10,11 +10,14 @@ import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
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.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Card
@@ -23,6 +26,7 @@ import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.OutlinedCard
@@ -41,7 +45,9 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.blur
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
@@ -52,6 +58,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.navigation.NavController
import eu.gaudian.translator.R
@@ -294,9 +301,17 @@ fun VocabularyCard(
}
)
Box(
modifier = Modifier
.fillMaxWidth()
.height(0.dp) // Take 0 structural space
.wrapContentHeight(unbounded = true) // Allow children to render at their full size
.zIndex(1f), // Ensure it draws over the bottom CardFace
contentAlignment = Alignment.Center
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(horizontal = 4.dp)
modifier = Modifier.fillMaxWidth()
) {
HorizontalDivider(
modifier = Modifier.weight(1f),
@@ -312,6 +327,7 @@ fun VocabularyCard(
}
}
}
}
CardFace(
modifier = backFaceModifier,
@@ -442,28 +458,52 @@ private fun FrequencyPill(zipfFrequency: Float?) {
if (zipfFrequency == null) return
val semanticColors = MaterialTheme.semanticColors
// Determine the label and color based on the Zipf value
val (label, color) = when {
zipfFrequency <= 3f -> Pair(stringResource(R.string.text_rare), semanticColors.wrongContainer)
zipfFrequency <= 4f -> Pair(stringResource(R.string.text_infrequent), semanticColors.stageGradient2)
zipfFrequency <= 4.5f -> Pair(stringResource(R.string.text_uncommon), semanticColors.stageGradient3)
zipfFrequency <= 2.0f -> Pair(stringResource(R.string.text_rare), semanticColors.wrongContainer)
zipfFrequency <= 3.0f -> Pair(stringResource(R.string.text_infrequent), semanticColors.stageGradient2)
zipfFrequency <= 4.0f -> Pair(stringResource(R.string.text_uncommon), semanticColors.stageGradient3)
zipfFrequency <= 5.5f -> Pair(stringResource(R.string.text_common), semanticColors.stageGradient4)
zipfFrequency <= 6.5f -> Pair(stringResource(R.string.text_frequent), semanticColors.stageGradient5)
zipfFrequency <= 7.5f -> Pair(stringResource(R.string.text_very_frequent), semanticColors.stageGradient6)
zipfFrequency <= 6.0f -> Pair(stringResource(R.string.text_frequent), semanticColors.stageGradient5)
else -> Pair(stringResource(R.string.text_very_frequent), semanticColors.successContainer)
}
// Normalize progress: Zipf 1.0 to 8.0 mapped to 0f to 1f
val progress = ((zipfFrequency - 1f) / (8f - 1f)).coerceIn(0f, 1f)
Column(
modifier = Modifier
.padding(horizontal = 4.dp)
.width(80.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Surface(
shape = RoundedCornerShape(16.dp),
color = color.copy(alpha = 0.5f),
modifier = Modifier.padding(horizontal = 4.dp)
color = color.copy(alpha = 0.2f),
) {
Text(
text = label,
style = MaterialTheme.typography.labelMedium,
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 2.dp)
)
}
Spacer(modifier = Modifier.height(4.dp))
// The "Slider" representation
LinearProgressIndicator(
progress = { progress },
modifier = Modifier
.fillMaxWidth()
.height(4.dp)
.clip(CircleShape),
color = color,
trackColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f),
strokeCap = StrokeCap.Round
)
}
}
@Composable

View File

@@ -986,15 +986,15 @@ class VocabularyViewModel @Inject constructor(
}
suspend fun getExampleForItem(itemId: Int, isFirstWord: Boolean, languageFirst: Language?, languageSecond: Language?): Pair<String, String>? {
Log.d(TAG, "Fetching example for item ID $itemId")
val item = getVocabularyItemById(itemId) ?: return null
val word = if (isFirstWord) item.wordFirst else item.wordSecond
val wordTranslation = if (!isFirstWord) item.wordFirst else item.wordSecond
if (languageFirst == null || languageSecond == null) return null
return dictionaryService.getExampleSentence(word, wordTranslation, languageFirst, languageSecond).getOrNull()
return if (isFirstWord) {
dictionaryService.getExampleSentence(item.wordFirst , item.wordSecond, languageFirst, languageSecond).getOrNull()
} else {
dictionaryService.getExampleSentence(item.wordSecond, item.wordFirst , languageSecond, languageFirst).getOrNull()
}
}
suspend fun fetchAndUpdateZipfFrequency(item: VocabularyItem): VocabularyItem {