Add and broadcast translation artifact version

We might not want to do translation chunk scaffolding
This commit is contained in:
Kgothatso Ngako
2026-07-28 09:21:16 +02:00
parent 8de05742d0
commit 02b2c9a4f8
7 changed files with 175 additions and 143 deletions

View File

@@ -11,6 +11,9 @@ import press.mantra.compose.database.model.MantraArtifactVersion
import press.mantra.compose.database.model.MantraChapter
import press.mantra.compose.database.model.MantraChunk
import press.mantra.compose.database.model.MantraDialect
import press.mantra.compose.database.model.MantraTranslationArtifactVersion
import press.mantra.compose.database.model.MantraTranslationChapter
import press.mantra.compose.database.model.MantraTranslationChunk
import press.mantra.compose.database.model.MarmotInnerEvent
import press.mantra.compose.database.model.intermdiate.LocalChatRoom
import press.mantra.compose.nostr.nip30303.ArtifactEvent
@@ -18,6 +21,9 @@ import press.mantra.compose.nostr.nip30303.ArtifactVersionEvent
import press.mantra.compose.nostr.nip30303.ChapterEvent
import press.mantra.compose.nostr.nip30303.ChunkEvent
import press.mantra.compose.nostr.nip30303.DialectEvent
import press.mantra.compose.nostr.nip30303.TranslationArtifactVersionEvent
import press.mantra.compose.nostr.nip30303.TranslationChapterEvent
import press.mantra.compose.nostr.nip30303.TranslationChunkEvent
import press.mantra.compose.nostr.nip30303.tags.ArtifactIdTag
import press.mantra.compose.repository.MantraRepository.Companion.DEFAULT_LICENSE
import press.mantra.compose.repository.MantraRepository.Companion.DEFAULT_VISIBILITY
@@ -303,6 +309,130 @@ abstract class MantraDao(
}
}
open suspend fun addTranslationArtifactVersion(
artifactId: String,
dialectId: String,
chatRoomId: String,
userPublicKey: HexKey,
): MarmotInnerEvent? {
val artifact = database.mantraArtifactDao().getArtifactById(artifactId) ?: return null
val version = database.mantraArtifactVersionDao()
.getArtifactVersionsByArtifactId(artifactId)
.firstOrNull() ?: return null
val dialect = database.mantraDialectDao().getDialectById(dialectId) ?: return null
// The translation is named after its dialect, and inherits the source
// artifact's visibility/license.
val translationVersionTemplate = TranslationArtifactVersionEvent.build(
artifactVersionId = version.id,
dialectId = dialectId,
name = dialect.name,
visibility = artifact.visibility,
license = artifact.license,
)
val translationVersion = MantraTranslationArtifactVersion.fromTranslationArtifactVersionEventTemplate(
translationArtifactVersionEventTemplate = translationVersionTemplate,
chatRoomId = chatRoomId,
userPublicKey = userPublicKey,
) ?: return null
return try {
database.mantraTranslationArtifactVersionDao().upsert(translationVersion)
val translationArtifactVersionInnerEvent = MarmotInnerEvent(
id = translationVersion.id,
publicKey = translationVersion.publicKey,
kind = TranslationArtifactVersionEvent.KIND,
createdAt = translationVersion.createdAt,
tags = translationVersionTemplate.tags,
content = translationVersionTemplate.content,
chatRoomId = translationVersion.chatRoomId,
)
sendMarmotInnerEvent(
chatRoomId = chatRoomId,
userPublicKey = userPublicKey,
text = "Added ${dialect.name} translation",
marmotInnerEvent = translationArtifactVersionInnerEvent
)
// Mirror the source structure: a translation chapter per chapter and
// an (untranslated) translation chunk per chunk.
val chapters = database.mantraChapterDao().getChaptersByArtifactVersionId(version.id)
chapters.forEach { chapter ->
val translationChapterTemplate = TranslationChapterEvent.build(
translationArtifactVersionId = translationVersion.id,
chapterId = chapter.id,
index = chapter.index,
)
val translationChapter = MantraTranslationChapter.fromTranslationChapterEventTemplate(
translationChapterEventTemplate = translationChapterTemplate,
chatRoomId = chatRoomId,
userPublicKey = userPublicKey,
) ?: return@forEach
database.mantraTranslationChapterDao().upsert(translationChapter)
val translationChapterInnerEvent = MarmotInnerEvent(
id = translationChapter.id,
publicKey = translationChapter.publicKey,
kind = TranslationChapterEvent.KIND,
createdAt = translationChapter.createdAt,
tags = translationChapterTemplate.tags,
content = translationChapterTemplate.content,
chatRoomId = translationChapter.chatRoomId,
)
sendMarmotInnerEvent(
chatRoomId = chatRoomId,
userPublicKey = userPublicKey,
text = "Prepared ${dialect.name} translation of the chapter ${chapter.name}",
marmotInnerEvent = translationChapterInnerEvent
)
// TODO: Don't scaffold translation chunks?
val chunks = database.mantraChunkDao().getChunksByChapterId(chapter.id)
chunks.forEach { chunk ->
val translationChunkTemplate = TranslationChunkEvent.build(
translationChapterId = translationChapter.id,
chunkId = chunk.id,
index = chunk.index,
text = "", // Untranslated scaffold; filled in during authoring.
)
val translationChunk = MantraTranslationChunk.fromTranslationChunkEventTemplate(
translationChunkEventTemplate = translationChunkTemplate,
chatRoomId = chatRoomId,
userPublicKey = userPublicKey,
) ?: return@forEach
database.mantraTranslationChunkDao().upsert(translationChunk)
val translationChunkInnerEvent = MarmotInnerEvent(
id = translationChunk.id,
publicKey = translationChunk.publicKey,
kind = TranslationChunkEvent.KIND,
createdAt = translationChunk.createdAt,
tags = translationChunkTemplate.tags,
content = translationChunkTemplate.content,
chatRoomId = translationChunk.chatRoomId,
)
database.marmotInnerEventDao().upsert(
translationChunkInnerEvent
)
sendMarmotInnerEvent(
chatRoomId = chatRoomId,
userPublicKey = userPublicKey,
text = "Prepared translation chunk ${chunk.index}",
marmotInnerEvent = translationChunkInnerEvent
)
}
}
translationArtifactVersionInnerEvent
} catch (error: Throwable) {
logger.e("Failed to add translation for artifact $artifactId", error)
null
}
}
private suspend fun sendMarmotInnerEvent(
localChatRoom: LocalChatRoom,
text: String,

View File

@@ -14,13 +14,8 @@ import press.mantra.compose.database.model.MantraTranslationChapter
import press.mantra.compose.database.model.MantraTranslationChunk
import press.mantra.compose.database.model.MarmotInnerEvent
import press.mantra.compose.database.model.intermdiate.LocalChatRoom
import press.mantra.compose.nostr.nip30303.ChapterEvent
import press.mantra.compose.nostr.nip30303.ChunkEvent
import press.mantra.compose.nostr.nip30303.TranslationArtifactVersionEvent
import press.mantra.compose.nostr.nip30303.TranslationChapterEvent
import press.mantra.compose.nostr.nip30303.TranslationChunkEvent
import press.mantra.compose.repository.MantraRepository
import press.mantra.compose.text.Markdown
class DatabaseMantraRepository(
val database: MantraDatabase,
@@ -44,109 +39,18 @@ class DatabaseMantraRepository(
override suspend fun getChaptersForArtifact(artifactId: String): List<MantraChapter> =
database.mantraChapterDao().getChaptersByArtifactId(artifactId)
override suspend fun addTranslation(
override suspend fun addTranslationArtifactVersion(
artifactId: String,
dialectId: String,
chatRoomId: String,
userPublicKey: HexKey,
): MantraTranslationArtifactVersion? {
val artifact = database.mantraArtifactDao().getArtifactById(artifactId) ?: return null
val version = database.mantraArtifactVersionDao()
.getArtifactVersionsByArtifactId(artifactId)
.firstOrNull() ?: return null
val dialect = database.mantraDialectDao().getDialectById(dialectId) ?: return null
// The translation is named after its dialect, and inherits the source
// artifact's visibility/license.
val translationVersionTemplate = TranslationArtifactVersionEvent.build(
artifactVersionId = version.id,
): MarmotInnerEvent? {
return database.mantraDao().addTranslationArtifactVersion(
artifactId = artifactId,
dialectId = dialectId,
name = dialect.name,
visibility = artifact.visibility,
license = artifact.license,
)
val translationVersion = MantraTranslationArtifactVersion.fromTranslationArtifactVersionEventTemplate(
translationArtifactVersionEventTemplate = translationVersionTemplate,
chatRoomId = chatRoomId,
userPublicKey = userPublicKey,
) ?: return null
return try {
database.mantraTranslationArtifactVersionDao().upsert(translationVersion)
database.marmotInnerEventDao().upsert(
MarmotInnerEvent(
id = translationVersion.id,
publicKey = translationVersion.publicKey,
kind = TranslationArtifactVersionEvent.KIND,
createdAt = translationVersion.createdAt,
tags = translationVersionTemplate.tags,
content = translationVersionTemplate.content,
chatRoomId = translationVersion.chatRoomId,
)
)
// Mirror the source structure: a translation chapter per chapter and
// an (untranslated) translation chunk per chunk.
val chapters = database.mantraChapterDao().getChaptersByArtifactVersionId(version.id)
chapters.forEach { chapter ->
val translationChapterTemplate = TranslationChapterEvent.build(
translationArtifactVersionId = translationVersion.id,
chapterId = chapter.id,
index = chapter.index,
)
val translationChapter = MantraTranslationChapter.fromTranslationChapterEventTemplate(
translationChapterEventTemplate = translationChapterTemplate,
chatRoomId = chatRoomId,
userPublicKey = userPublicKey,
) ?: return@forEach
database.mantraTranslationChapterDao().upsert(translationChapter)
database.marmotInnerEventDao().upsert(
MarmotInnerEvent(
id = translationChapter.id,
publicKey = translationChapter.publicKey,
kind = TranslationChapterEvent.KIND,
createdAt = translationChapter.createdAt,
tags = translationChapterTemplate.tags,
content = translationChapterTemplate.content,
chatRoomId = translationChapter.chatRoomId,
)
)
val chunks = database.mantraChunkDao().getChunksByChapterId(chapter.id)
chunks.forEach { chunk ->
val translationChunkTemplate = TranslationChunkEvent.build(
translationChapterId = translationChapter.id,
chunkId = chunk.id,
index = chunk.index,
text = "", // Untranslated scaffold; filled in during authoring.
)
val translationChunk = MantraTranslationChunk.fromTranslationChunkEventTemplate(
translationChunkEventTemplate = translationChunkTemplate,
chatRoomId = chatRoomId,
userPublicKey = userPublicKey,
) ?: return@forEach
database.mantraTranslationChunkDao().upsert(translationChunk)
database.marmotInnerEventDao().upsert(
MarmotInnerEvent(
id = translationChunk.id,
publicKey = translationChunk.publicKey,
kind = TranslationChunkEvent.KIND,
createdAt = translationChunk.createdAt,
tags = translationChunkTemplate.tags,
content = translationChunkTemplate.content,
chatRoomId = translationChunk.chatRoomId,
)
)
}
}
translationVersion
} catch (error: Throwable) {
logger.e("Failed to add translation for artifact $artifactId", error)
null
}
userPublicKey = userPublicKey
)
}
override suspend fun getChapter(id: String): MantraChapter? =

View File

@@ -60,12 +60,12 @@ interface MantraRepository {
* MantraTranslationChunk (empty text) for each existing chunk. Returns null
* when the artifact has no version to translate.
*/
suspend fun addTranslation(
suspend fun addTranslationArtifactVersion(
artifactId: String,
dialectId: String,
chatRoomId: String,
userPublicKey: HexKey,
): MantraTranslationArtifactVersion?
): MarmotInnerEvent?
/**
* Add a chapter (markdown [originalText]) to the artifact's latest version.
@@ -150,12 +150,12 @@ interface MantraRepository {
override suspend fun getTranslationChunks(translationChapterId: String): List<MantraTranslationChunk> = emptyList()
override suspend fun addTranslation(
override suspend fun addTranslationArtifactVersion(
artifactId: String,
dialectId: String,
chatRoomId: String,
userPublicKey: HexKey,
): MantraTranslationArtifactVersion? = null
): MarmotInnerEvent? = null
override suspend fun addChapter(
artifactId: String,

View File

@@ -50,36 +50,36 @@ import press.mantra.compose.ui.composable.navigation.routes.ArtifactDetailRoute
import press.mantra.compose.ui.composable.navigation.routes.Route
import press.mantra.compose.ui.composable.widgets.LoadingDataIndicator
import press.mantra.compose.ui.theme.TorchTheme
import press.mantra.compose.ui.view.model.AddTranslationViewModel
import press.mantra.compose.ui.view.state.AddTranslationUIState
import press.mantra.compose.ui.view.model.AddTranslationArtifactVersionViewModel
import press.mantra.compose.ui.view.state.AddTranslationArtifactVersionUIState
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
@Composable
fun AddTranslationScreen(
fun AddTranslationArtifactVersionScreen(
activeUserPublicKey: HexKey,
artifactId: String,
chatRoomId: String,
relayHint: String?,
initialAddTranslationUIState: AddTranslationUIState = AddTranslationUIState.Loading,
initialAddTranslationArtifactVersionUIState: AddTranslationArtifactVersionUIState = AddTranslationArtifactVersionUIState.Loading,
mantraRepository: MantraRepository,
chatRepository: ChatRepository,
onNavigateToRoute: (Route) -> Unit,
onNavigateBack: () -> Unit,
) {
val addTranslationViewModel: AddTranslationViewModel = viewModel(
factory = AddTranslationViewModel.factory(
val addTranslationArtifactVersionViewModel: AddTranslationArtifactVersionViewModel = viewModel(
factory = AddTranslationArtifactVersionViewModel.factory(
activeUserPublicKey = activeUserPublicKey,
artifactId = artifactId,
chatRoomId = chatRoomId,
relayHint = relayHint,
initialAddTranslationUIState = initialAddTranslationUIState,
initialAddTranslationArtifactVersionUIState = initialAddTranslationArtifactVersionUIState,
mantraRepository = mantraRepository,
chatRepository = chatRepository
)
)
when (val addTranslationUIState = addTranslationViewModel.addTranslationUIState) {
is AddTranslationUIState.Error -> {
when (val addTranslationUIState = addTranslationArtifactVersionViewModel.addTranslationArtifactVersionUIState) {
is AddTranslationArtifactVersionUIState.Error -> {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
@@ -89,7 +89,7 @@ fun AddTranslationScreen(
}
}
is AddTranslationUIState.Loaded -> {
is AddTranslationArtifactVersionUIState.Loaded -> {
val dialectNameFieldState = rememberTextFieldState()
val dialectCountryFieldState = rememberTextFieldState()
val dialectLanguageFieldState = rememberTextFieldState()
@@ -118,7 +118,7 @@ fun AddTranslationScreen(
floatingActionButton = {
ExtendedFloatingActionButton(
onClick = {
addTranslationViewModel.addTranslation(
addTranslationArtifactVersionViewModel.addTranslation(
localChatRoom = addTranslationUIState.localChatRoom,
existingDialectId = selectedDialectId,
dialectNameField = dialectNameFieldState,
@@ -228,7 +228,7 @@ fun AddTranslationScreen(
}
}
AddTranslationUIState.Loading -> {
AddTranslationArtifactVersionUIState.Loading -> {
Column(
modifier = Modifier.fillMaxWidth().padding(20.dp),
horizontalAlignment = Alignment.CenterHorizontally,
@@ -247,23 +247,23 @@ fun AddTranslationScreen(
}
LaunchedEffect(true) {
if (initialAddTranslationUIState == AddTranslationUIState.Loading) {
addTranslationViewModel.initiateAddTranslation()
if (initialAddTranslationArtifactVersionUIState == AddTranslationArtifactVersionUIState.Loading) {
addTranslationArtifactVersionViewModel.initiateAddTranslation()
}
}
}
@Preview
@Composable
private fun AddTranslationScreenPreview() {
private fun AddTranslationArtifactVersionScreenPreview() {
TorchTheme {
Surface(modifier = Modifier.fillMaxSize()) {
AddTranslationScreen(
AddTranslationArtifactVersionScreen(
activeUserPublicKey = "",
artifactId = "artifactId",
chatRoomId = "chatRoomId",
relayHint = null,
initialAddTranslationUIState = AddTranslationUIState.Loaded(
initialAddTranslationArtifactVersionUIState = AddTranslationArtifactVersionUIState.Loaded(
localChatRoom = LocalChatRoom(
chatRoom = ChatRoom(
id = "",

View File

@@ -99,7 +99,7 @@ import press.mantra.compose.ui.composable.navigation.routes.TranslationChapterRo
import press.mantra.compose.ui.composable.navigation.routes.TranslationArtifactVersionDetailRoute
import press.mantra.compose.ui.composable.ArtifactDetailScreen
import press.mantra.compose.ui.composable.AddChapterScreen
import press.mantra.compose.ui.composable.AddTranslationScreen
import press.mantra.compose.ui.composable.AddTranslationArtifactVersionScreen
import press.mantra.compose.ui.composable.ChapterDetailScreen
import press.mantra.compose.ui.composable.TranslateChunkScreen
import press.mantra.compose.ui.composable.TranslationChapterScreen
@@ -803,7 +803,7 @@ fun MantraNavHost(
composable<AddTranslationRoute> { backStackEntry ->
val route = backStackEntry.toRoute<AddTranslationRoute>()
AddTranslationScreen(
AddTranslationArtifactVersionScreen(
activeUserPublicKey = route.activeUserPublicKey,
artifactId = route.artifactId,
chatRoomId = route.chatRoomId,

View File

@@ -16,24 +16,22 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.launch
import press.mantra.compose.database.model.ChatRoom
import press.mantra.compose.database.model.intermdiate.LocalChatRoom
import press.mantra.compose.repository.ChatRepository
import press.mantra.compose.repository.MantraRepository
import press.mantra.compose.repository.MarmotRepository
import press.mantra.compose.ui.view.state.AddTranslationUIState
import press.mantra.compose.ui.view.state.AddTranslationArtifactVersionUIState
class AddTranslationViewModel(
class AddTranslationArtifactVersionViewModel(
val artifactId: String,
val chatRoomId: String,
val activeUserPublicKey: HexKey,
val relayHint: String?,
initialAddTranslationUIState: AddTranslationUIState,
initialAddTranslationArtifactVersionUIState: AddTranslationArtifactVersionUIState,
val mantraRepository: MantraRepository,
val chatRepository: ChatRepository,
): ViewModel() {
var addTranslationUIState: AddTranslationUIState by mutableStateOf(initialAddTranslationUIState)
var addTranslationArtifactVersionUIState: AddTranslationArtifactVersionUIState by mutableStateOf(initialAddTranslationArtifactVersionUIState)
private set
private val logger = Logger.withTag(TAG)
@@ -44,10 +42,10 @@ class AddTranslationViewModel(
viewModelScope.launch(Dispatchers.IO) {
val artifact = mantraRepository.getArtifact(artifactId)
val localChatRoom = chatRepository.getChatRoomByIdentifier(chatRoomId)
addTranslationUIState = if (artifact == null || localChatRoom == null) {
AddTranslationUIState.Error("Couldn't find the artifact")
addTranslationArtifactVersionUIState = if (artifact == null || localChatRoom == null) {
AddTranslationArtifactVersionUIState.Error("Couldn't find the artifact")
} else {
AddTranslationUIState.Loaded(
AddTranslationArtifactVersionUIState.Loaded(
artifact = artifact,
localChatRoom = localChatRoom,
dialects = mantraRepository.getDialects(chatRoomId),
@@ -96,10 +94,10 @@ class AddTranslationViewModel(
userPublicKey = activeUserPublicKey,
)?.id ?: return@runCatching null
} else {
existingDialectId!!
existingDialectId
}
mantraRepository.addTranslation(
mantraRepository.addTranslationArtifactVersion(
artifactId = artifactId,
dialectId = dialectId,
chatRoomId = chatRoomId,
@@ -136,17 +134,17 @@ class AddTranslationViewModel(
artifactId: String,
chatRoomId: String,
relayHint: String?,
initialAddTranslationUIState: AddTranslationUIState = AddTranslationUIState.Loading,
initialAddTranslationArtifactVersionUIState: AddTranslationArtifactVersionUIState = AddTranslationArtifactVersionUIState.Loading,
mantraRepository: MantraRepository,
chatRepository: ChatRepository
): ViewModelProvider.Factory = viewModelFactory {
initializer {
AddTranslationViewModel(
AddTranslationArtifactVersionViewModel(
activeUserPublicKey = activeUserPublicKey,
artifactId = artifactId,
chatRoomId = chatRoomId,
relayHint = relayHint,
initialAddTranslationUIState = initialAddTranslationUIState,
initialAddTranslationArtifactVersionUIState = initialAddTranslationArtifactVersionUIState,
mantraRepository = mantraRepository,
chatRepository = chatRepository
)

View File

@@ -4,16 +4,16 @@ import press.mantra.compose.database.model.MantraArtifact
import press.mantra.compose.database.model.MantraDialect
import press.mantra.compose.database.model.intermdiate.LocalChatRoom
sealed interface AddTranslationUIState {
sealed interface AddTranslationArtifactVersionUIState {
data class Loaded(
val localChatRoom: LocalChatRoom,
val artifact: MantraArtifact,
val dialects: List<MantraDialect> = emptyList(),
): AddTranslationUIState
): AddTranslationArtifactVersionUIState
data class Error(
val message: String
): AddTranslationUIState
): AddTranslationArtifactVersionUIState
data object Loading: AddTranslationUIState
data object Loading: AddTranslationArtifactVersionUIState
}