Merge pull request #2176 from vitorpamplona/claude/ai-post-writing-help-CDPPw
Add AI writing assistance with ML Kit integration
This commit is contained in:
@@ -338,6 +338,11 @@ dependencies {
|
||||
// Google services model the translate text
|
||||
playImplementation libs.google.mlkit.translate
|
||||
|
||||
// On-device AI writing assistance (Gemini Nano via AICore)
|
||||
playImplementation libs.google.mlkit.genai.proofreading
|
||||
playImplementation libs.google.mlkit.genai.prompt
|
||||
playImplementation libs.google.mlkit.genai.rewriting
|
||||
|
||||
// PushNotifications
|
||||
playImplementation platform(libs.firebase.bom)
|
||||
playImplementation libs.firebase.messaging
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.service.ai
|
||||
|
||||
class NoOpWritingAssistant : WritingAssistant {
|
||||
override suspend fun checkAvailability(): WritingAssistantStatus = WritingAssistantStatus.Unavailable
|
||||
|
||||
override suspend fun transform(
|
||||
text: String,
|
||||
tone: WritingTone,
|
||||
): WritingResult = WritingResult(text, text, tone)
|
||||
|
||||
override fun close() {}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.service.ai
|
||||
|
||||
import android.content.Context
|
||||
|
||||
object WritingAssistantFactory {
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
fun create(context: Context): WritingAssistant = NoOpWritingAssistant()
|
||||
}
|
||||
@@ -38,6 +38,7 @@ data class UiSettings(
|
||||
val dontAskForNotificationPermissions: Boolean = false,
|
||||
val featureSet: FeatureSetType = FeatureSetType.SIMPLIFIED,
|
||||
val gallerySet: ProfileGalleryType = ProfileGalleryType.CLASSIC,
|
||||
val automaticallyProposeAiImprovements: BooleanType = BooleanType.ALWAYS,
|
||||
)
|
||||
|
||||
enum class ThemeType(
|
||||
|
||||
@@ -38,6 +38,7 @@ class UiSettingsFlow(
|
||||
val dontAskForNotificationPermissions: MutableStateFlow<Boolean> = MutableStateFlow(false),
|
||||
val featureSet: MutableStateFlow<FeatureSetType> = MutableStateFlow(FeatureSetType.SIMPLIFIED),
|
||||
val gallerySet: MutableStateFlow<ProfileGalleryType> = MutableStateFlow(ProfileGalleryType.CLASSIC),
|
||||
val automaticallyProposeAiImprovements: MutableStateFlow<BooleanType> = MutableStateFlow(BooleanType.ALWAYS),
|
||||
) {
|
||||
val listOfFlows: List<Flow<Any?>> =
|
||||
listOf<Flow<Any?>>(
|
||||
@@ -52,6 +53,7 @@ class UiSettingsFlow(
|
||||
dontAskForNotificationPermissions,
|
||||
featureSet,
|
||||
gallerySet,
|
||||
automaticallyProposeAiImprovements,
|
||||
)
|
||||
|
||||
// emits at every change in any of the propertyes.
|
||||
@@ -69,6 +71,7 @@ class UiSettingsFlow(
|
||||
flows[8] as Boolean,
|
||||
flows[9] as FeatureSetType,
|
||||
flows[10] as ProfileGalleryType,
|
||||
flows[11] as BooleanType,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -85,6 +88,7 @@ class UiSettingsFlow(
|
||||
dontAskForNotificationPermissions.value,
|
||||
featureSet.value,
|
||||
gallerySet.value,
|
||||
automaticallyProposeAiImprovements.value,
|
||||
)
|
||||
|
||||
fun update(torSettings: UiSettings): Boolean {
|
||||
@@ -134,6 +138,10 @@ class UiSettingsFlow(
|
||||
gallerySet.tryEmit(torSettings.gallerySet)
|
||||
any = true
|
||||
}
|
||||
if (automaticallyProposeAiImprovements.value != torSettings.automaticallyProposeAiImprovements) {
|
||||
automaticallyProposeAiImprovements.tryEmit(torSettings.automaticallyProposeAiImprovements)
|
||||
any = true
|
||||
}
|
||||
|
||||
return any
|
||||
}
|
||||
@@ -164,6 +172,7 @@ class UiSettingsFlow(
|
||||
MutableStateFlow(uiSettings.dontAskForNotificationPermissions),
|
||||
MutableStateFlow(uiSettings.featureSet),
|
||||
MutableStateFlow(uiSettings.gallerySet),
|
||||
MutableStateFlow(uiSettings.automaticallyProposeAiImprovements),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+3
@@ -102,6 +102,7 @@ class UiSharedPreferences(
|
||||
val UI_DONT_ASK_FOR_NOTIFICATION_PERMISSIONS = booleanPreferencesKey("ui.dont_ask_for_notification_permissions")
|
||||
val UI_FEATURE_SET = stringPreferencesKey("ui.feature_set")
|
||||
val UI_GALLERY_SET = stringPreferencesKey("ui.gallery_set")
|
||||
val UI_PROPOSE_AI_IMPROVEMENTS = stringPreferencesKey("ui.propose_ai_improvements")
|
||||
|
||||
suspend fun uiPreferences(context: Context): UiSettings? =
|
||||
try {
|
||||
@@ -120,6 +121,7 @@ class UiSharedPreferences(
|
||||
dontAskForNotificationPermissions = preferences[UI_DONT_ASK_FOR_NOTIFICATION_PERMISSIONS] ?: false,
|
||||
featureSet = preferences[UI_FEATURE_SET]?.let { FeatureSetType.valueOf(it) } ?: FeatureSetType.SIMPLIFIED,
|
||||
gallerySet = preferences[UI_GALLERY_SET]?.let { ProfileGalleryType.valueOf(it) } ?: ProfileGalleryType.CLASSIC,
|
||||
automaticallyProposeAiImprovements = preferences[UI_PROPOSE_AI_IMPROVEMENTS]?.let { BooleanType.valueOf(it) } ?: BooleanType.ALWAYS,
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
@@ -155,6 +157,7 @@ class UiSharedPreferences(
|
||||
preferences[UI_DONT_ASK_FOR_NOTIFICATION_PERMISSIONS] = sharedSettings.dontAskForNotificationPermissions
|
||||
preferences[UI_FEATURE_SET] = sharedSettings.featureSet.name
|
||||
preferences[UI_GALLERY_SET] = sharedSettings.gallerySet.name
|
||||
preferences[UI_PROPOSE_AI_IMPROVEMENTS] = sharedSettings.automaticallyProposeAiImprovements.name
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.service.ai
|
||||
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
/**
|
||||
* TODO: Remove before shipping. Debug-only mock for testing the AI writing help UI
|
||||
* on devices without Gemini Nano / AICore support.
|
||||
*/
|
||||
class MockWritingAssistant : WritingAssistant {
|
||||
override suspend fun checkAvailability(): WritingAssistantStatus = WritingAssistantStatus.Available
|
||||
|
||||
override suspend fun transform(
|
||||
text: String,
|
||||
tone: WritingTone,
|
||||
): WritingResult {
|
||||
delay(800)
|
||||
|
||||
val transformed =
|
||||
when (tone) {
|
||||
WritingTone.CORRECT -> {
|
||||
correctMock(text)
|
||||
}
|
||||
|
||||
WritingTone.REPHRASE -> {
|
||||
"Here's another way to put it: $text"
|
||||
}
|
||||
|
||||
WritingTone.SHORTER -> {
|
||||
text
|
||||
.split(".")
|
||||
.firstOrNull()
|
||||
?.trim()
|
||||
?.plus(".") ?: text
|
||||
}
|
||||
|
||||
WritingTone.ELABORATE -> {
|
||||
"$text Furthermore, this point deserves deeper consideration and nuance."
|
||||
}
|
||||
|
||||
WritingTone.FRIENDLY -> {
|
||||
"Hey! $text Hope that makes sense! :)"
|
||||
}
|
||||
|
||||
WritingTone.PROFESSIONAL -> {
|
||||
"I would like to bring to your attention the following: $text"
|
||||
}
|
||||
|
||||
WritingTone.MORE_DIRECT -> {
|
||||
text.replace("I think ", "").replace("maybe ", "").replace("perhaps ", "")
|
||||
}
|
||||
|
||||
WritingTone.PUNCHY -> {
|
||||
text.uppercase().replace(".", "!")
|
||||
}
|
||||
|
||||
WritingTone.EMOJIFY -> {
|
||||
"$text \uD83D\uDE80\uD83D\uDD25\u2728"
|
||||
}
|
||||
}
|
||||
|
||||
return WritingResult(
|
||||
originalText = text,
|
||||
transformedText = transformed,
|
||||
tone = tone,
|
||||
)
|
||||
}
|
||||
|
||||
private fun correctMock(text: String): String =
|
||||
text
|
||||
.replace("teh ", "the ")
|
||||
.replace("dont ", "don't ")
|
||||
.replace("cant ", "can't ")
|
||||
.replace("wont ", "won't ")
|
||||
.replace("i ", "I ")
|
||||
|
||||
override fun close() {}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.service.ai
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
|
||||
interface WritingAssistant {
|
||||
suspend fun checkAvailability(): WritingAssistantStatus
|
||||
|
||||
suspend fun transform(
|
||||
text: String,
|
||||
tone: WritingTone,
|
||||
): WritingResult
|
||||
|
||||
fun close()
|
||||
}
|
||||
|
||||
enum class WritingTone {
|
||||
CORRECT,
|
||||
REPHRASE,
|
||||
SHORTER,
|
||||
ELABORATE,
|
||||
FRIENDLY,
|
||||
PROFESSIONAL,
|
||||
MORE_DIRECT,
|
||||
PUNCHY,
|
||||
EMOJIFY,
|
||||
}
|
||||
|
||||
sealed class WritingAssistantStatus {
|
||||
data object Available : WritingAssistantStatus()
|
||||
|
||||
data object Unavailable : WritingAssistantStatus()
|
||||
|
||||
data object Downloading : WritingAssistantStatus()
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class WritingResult(
|
||||
val originalText: String,
|
||||
val transformedText: String,
|
||||
val tone: WritingTone,
|
||||
)
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.note.creators.aihelp
|
||||
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.AutoAwesome
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
|
||||
@Composable
|
||||
fun AiWritingHelpButton(
|
||||
isActive: Boolean,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
IconButton(
|
||||
onClick = { onClick() },
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.AutoAwesome,
|
||||
contentDescription = stringRes(R.string.ai_writing_help),
|
||||
modifier = Modifier.height(22.dp),
|
||||
tint =
|
||||
if (isActive) {
|
||||
MaterialTheme.colorScheme.primary
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onBackground
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.note.creators.aihelp
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.service.ai.WritingResult
|
||||
import com.vitorpamplona.amethyst.service.ai.WritingTone
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
|
||||
@Composable
|
||||
fun AiWritingHelpPanel(
|
||||
isVisible: Boolean,
|
||||
readyResults: Map<WritingTone, WritingResult>,
|
||||
selectedResult: WritingResult?,
|
||||
onToneSelected: (WritingTone) -> Unit,
|
||||
onApply: () -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = isVisible,
|
||||
enter = expandVertically(),
|
||||
exit = shrinkVertically(),
|
||||
) {
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 8.dp),
|
||||
) {
|
||||
selectedResult?.let {
|
||||
AiResultCard(
|
||||
result = it,
|
||||
onApply = onApply,
|
||||
onDismiss = onDismiss,
|
||||
)
|
||||
}
|
||||
|
||||
ToneChipRow(
|
||||
readyTones = readyResults.keys,
|
||||
selectedTone = selectedResult?.tone,
|
||||
onToneSelected = onToneSelected,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AiResultCard(
|
||||
result: WritingResult,
|
||||
onApply: () -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 4.dp)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant)
|
||||
.padding(12.dp),
|
||||
) {
|
||||
Text(
|
||||
text = result.transformedText,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(max = 150.dp)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 8.dp),
|
||||
horizontalArrangement = Arrangement.End,
|
||||
) {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(stringRes(R.string.ai_writing_dismiss))
|
||||
}
|
||||
OutlinedButton(onClick = onApply) {
|
||||
Text(stringRes(R.string.ai_writing_use_this))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ToneChipRow(
|
||||
readyTones: Set<WritingTone>,
|
||||
selectedTone: WritingTone?,
|
||||
onToneSelected: (WritingTone) -> Unit,
|
||||
) {
|
||||
val scrollState = rememberScrollState()
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.horizontalScroll(scrollState)
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
) {
|
||||
WritingTone.entries.forEach { tone ->
|
||||
if (tone in readyTones) {
|
||||
FilterChip(
|
||||
selected = tone == selectedTone,
|
||||
onClick = { onToneSelected(tone) },
|
||||
label = { Text(toneDisplayName(tone)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun toneDisplayName(tone: WritingTone): String =
|
||||
when (tone) {
|
||||
WritingTone.CORRECT -> stringRes(R.string.ai_tone_correct)
|
||||
WritingTone.REPHRASE -> stringRes(R.string.ai_tone_rephrase)
|
||||
WritingTone.SHORTER -> stringRes(R.string.ai_tone_shorter)
|
||||
WritingTone.ELABORATE -> stringRes(R.string.ai_tone_elaborate)
|
||||
WritingTone.FRIENDLY -> stringRes(R.string.ai_tone_friendly)
|
||||
WritingTone.PROFESSIONAL -> stringRes(R.string.ai_tone_professional)
|
||||
WritingTone.MORE_DIRECT -> stringRes(R.string.ai_tone_more_direct)
|
||||
WritingTone.PUNCHY -> stringRes(R.string.ai_tone_punchy)
|
||||
WritingTone.EMOJIFY -> stringRes(R.string.ai_tone_emojify)
|
||||
}
|
||||
+14
@@ -82,6 +82,7 @@ import com.vitorpamplona.amethyst.ui.navigation.navs.Nav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.PostingTopBar
|
||||
import com.vitorpamplona.amethyst.ui.note.BaseUserPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.aihelp.AiWritingHelpPanel
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.contentWarning.ContentSensitivityExplainer
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.contentWarning.MarkAsSensitiveButton
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.ShowEmojiSuggestionList
|
||||
@@ -144,6 +145,10 @@ fun ShortNotePostScreen(
|
||||
val context = LocalContext.current
|
||||
val activity = context.getActivity()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
postViewModel.initWritingAssistant(context)
|
||||
}
|
||||
|
||||
LaunchedEffect(postViewModel, accountViewModel) {
|
||||
val baseReplyTo = baseReplyToId?.let { accountViewModel.getNoteIfExists(it) }
|
||||
val quote = quoteId?.let { accountViewModel.getNoteIfExists(it) }
|
||||
@@ -556,6 +561,15 @@ private fun NewPostScreenBody(
|
||||
)
|
||||
}
|
||||
|
||||
AiWritingHelpPanel(
|
||||
isVisible = postViewModel.showAiPanel,
|
||||
readyResults = postViewModel.aiResults,
|
||||
selectedResult = postViewModel.aiSelectedResult,
|
||||
onToneSelected = postViewModel::selectAiResult,
|
||||
onApply = postViewModel::applyAiResult,
|
||||
onDismiss = postViewModel::dismissAiResult,
|
||||
)
|
||||
|
||||
BottomRowActions(postViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
+106
@@ -41,9 +41,16 @@ import com.vitorpamplona.amethyst.commons.compose.replaceCurrentWord
|
||||
import com.vitorpamplona.amethyst.commons.compose.setTextAndPlaceCursorAtBeginning
|
||||
import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState.EmojiMedia
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.BooleanType
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.ai.MockWritingAssistant
|
||||
import com.vitorpamplona.amethyst.service.ai.WritingAssistant
|
||||
import com.vitorpamplona.amethyst.service.ai.WritingAssistantFactory
|
||||
import com.vitorpamplona.amethyst.service.ai.WritingAssistantStatus
|
||||
import com.vitorpamplona.amethyst.service.ai.WritingResult
|
||||
import com.vitorpamplona.amethyst.service.ai.WritingTone
|
||||
import com.vitorpamplona.amethyst.service.location.LocationState
|
||||
import com.vitorpamplona.amethyst.service.uploads.CompressorQuality
|
||||
import com.vitorpamplona.amethyst.service.uploads.MediaCompressor
|
||||
@@ -140,7 +147,13 @@ import com.vitorpamplona.quartz.utils.Log
|
||||
import com.vitorpamplona.quartz.utils.RandomInstance
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -291,6 +304,96 @@ open class ShortNotePostViewModel :
|
||||
// Anonymous Reply
|
||||
var wantsAnonymousPost by mutableStateOf(false)
|
||||
|
||||
// AI Writing Help for testing
|
||||
private val useMockAi = false
|
||||
|
||||
var aiResults by mutableStateOf<Map<WritingTone, WritingResult>>(emptyMap())
|
||||
var aiSelectedResult by mutableStateOf<WritingResult?>(null)
|
||||
var aiStatus by mutableStateOf<WritingAssistantStatus>(WritingAssistantStatus.Unavailable)
|
||||
private var writingAssistant: WritingAssistant? = null
|
||||
private var aiComputeJob: Job? = null
|
||||
private var lastComputedText: String = ""
|
||||
|
||||
val showAiPanel: Boolean
|
||||
get() {
|
||||
val prefEnabled =
|
||||
accountViewModel.settings.uiSettingsFlow.automaticallyProposeAiImprovements.value ==
|
||||
BooleanType.ALWAYS
|
||||
return prefEnabled && aiStatus is WritingAssistantStatus.Available && aiResults.isNotEmpty()
|
||||
}
|
||||
|
||||
fun initWritingAssistant(context: android.content.Context) {
|
||||
if (writingAssistant == null) {
|
||||
writingAssistant =
|
||||
if (useMockAi) {
|
||||
MockWritingAssistant()
|
||||
} else {
|
||||
WritingAssistantFactory.create(context)
|
||||
}
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
aiStatus = writingAssistant?.checkAvailability() ?: WritingAssistantStatus.Unavailable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun precomputeAiResults() {
|
||||
val assistant = writingAssistant ?: return
|
||||
if (aiStatus !is WritingAssistantStatus.Available) return
|
||||
val prefEnabled =
|
||||
accountViewModel.settings.uiSettingsFlow.automaticallyProposeAiImprovements.value ==
|
||||
BooleanType.ALWAYS
|
||||
if (!prefEnabled) return
|
||||
|
||||
val text = message.text.toString().trim()
|
||||
if (text.isBlank() || text == lastComputedText) return
|
||||
|
||||
aiComputeJob?.cancel()
|
||||
aiResults = emptyMap()
|
||||
aiSelectedResult = null
|
||||
|
||||
aiComputeJob =
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
delay(1000)
|
||||
lastComputedText = text
|
||||
|
||||
val results =
|
||||
coroutineScope {
|
||||
WritingTone.entries
|
||||
.map { tone ->
|
||||
async {
|
||||
try {
|
||||
assistant.transform(text, tone)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
null
|
||||
}
|
||||
}
|
||||
}.awaitAll()
|
||||
.filterNotNull()
|
||||
.associateBy { it.tone }
|
||||
}
|
||||
aiResults = results
|
||||
}
|
||||
}
|
||||
|
||||
fun selectAiResult(tone: WritingTone) {
|
||||
aiSelectedResult = aiResults[tone]
|
||||
}
|
||||
|
||||
fun applyAiResult() {
|
||||
aiSelectedResult?.let {
|
||||
message.setTextAndPlaceCursorAtEnd(it.transformedText)
|
||||
aiSelectedResult = null
|
||||
aiResults = emptyMap()
|
||||
lastComputedText = ""
|
||||
draftTag.newVersion()
|
||||
}
|
||||
}
|
||||
|
||||
fun dismissAiResult() {
|
||||
aiSelectedResult = null
|
||||
}
|
||||
|
||||
fun lnAddress(): String? = account.userProfile().lnAddress()
|
||||
|
||||
fun hasLnAddress(): Boolean = account.userProfile().lnAddress() != null
|
||||
@@ -1131,6 +1234,7 @@ open class ShortNotePostViewModel :
|
||||
emojiSuggestions?.processCurrentWord(lastWord)
|
||||
}
|
||||
|
||||
precomputeAiResults()
|
||||
draftTag.newVersion()
|
||||
}
|
||||
|
||||
@@ -1372,6 +1476,8 @@ open class ShortNotePostViewModel :
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
writingAssistant?.close()
|
||||
writingAssistant = null
|
||||
Log.d("Init") { "OnCleared: ${this.javaClass.simpleName}" }
|
||||
}
|
||||
|
||||
|
||||
+21
@@ -121,6 +121,7 @@ fun SettingsScreen(sharedPrefs: UiSettingsFlow) {
|
||||
ImmersiveScrollingChoice(sharedPrefs)
|
||||
FeatureSetChoice(sharedPrefs)
|
||||
GalleryChoice(sharedPrefs)
|
||||
AiWritingHelpChoice(sharedPrefs)
|
||||
PushNotificationSettingsRow(sharedPrefs)
|
||||
}
|
||||
}
|
||||
@@ -366,6 +367,26 @@ fun GalleryChoice(sharedPrefs: UiSettingsFlow) {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AiWritingHelpChoice(sharedPrefs: UiSettingsFlow) {
|
||||
val aiIndex by sharedPrefs.automaticallyProposeAiImprovements.collectAsState()
|
||||
|
||||
val booleanItems =
|
||||
persistentListOf(
|
||||
TitleExplainer(stringRes(ConnectivityType.ALWAYS.resourceId)),
|
||||
TitleExplainer(stringRes(ConnectivityType.NEVER.resourceId)),
|
||||
)
|
||||
|
||||
SettingsRow(
|
||||
R.string.ai_writing_setting_title,
|
||||
R.string.ai_writing_setting_description,
|
||||
booleanItems,
|
||||
aiIndex.screenCode,
|
||||
) {
|
||||
sharedPrefs.automaticallyProposeAiImprovements.tryEmit(parseBooleanType(it))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SettingsRow(
|
||||
name: Int,
|
||||
|
||||
@@ -2199,4 +2199,19 @@
|
||||
<string name="relay_members_removed">%1$d members removed from relay</string>
|
||||
<string name="relay_join_request">Relay join request</string>
|
||||
<string name="relay_leave_request">Relay leave request</string>
|
||||
|
||||
<string name="ai_writing_help">AI Writing Help</string>
|
||||
<string name="ai_writing_setting_title">Propose text improvements</string>
|
||||
<string name="ai_writing_setting_description">Uses an on-device AI model to propose text corrections and tone changes.</string>
|
||||
<string name="ai_writing_use_this">Use This</string>
|
||||
<string name="ai_writing_dismiss">Dismiss</string>
|
||||
<string name="ai_tone_correct">Correct</string>
|
||||
<string name="ai_tone_rephrase">Rephrase</string>
|
||||
<string name="ai_tone_shorter">Shorter</string>
|
||||
<string name="ai_tone_elaborate">Elaborate</string>
|
||||
<string name="ai_tone_friendly">Friendly</string>
|
||||
<string name="ai_tone_professional">Professional</string>
|
||||
<string name="ai_tone_more_direct">More Direct</string>
|
||||
<string name="ai_tone_punchy">Punchy</string>
|
||||
<string name="ai_tone_emojify">+ Emoji</string>
|
||||
</resources>
|
||||
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.service.ai
|
||||
|
||||
import android.content.Context
|
||||
import com.google.android.gms.tasks.Tasks
|
||||
import com.google.mlkit.genai.common.FeatureStatus
|
||||
import com.google.mlkit.genai.proofreading.Proofreader
|
||||
import com.google.mlkit.genai.proofreading.ProofreaderOptions
|
||||
import com.google.mlkit.genai.proofreading.Proofreading
|
||||
import com.google.mlkit.genai.proofreading.ProofreadingRequest
|
||||
import com.google.mlkit.genai.rewriting.Rewriter
|
||||
import com.google.mlkit.genai.rewriting.RewriterOptions
|
||||
import com.google.mlkit.genai.rewriting.Rewriting
|
||||
import com.google.mlkit.genai.rewriting.RewritingRequest
|
||||
import com.vitorpamplona.amethyst.service.lang.LanguageTranslatorService
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class MLKitWritingAssistant(
|
||||
private val context: Context,
|
||||
) : WritingAssistant {
|
||||
private var rewriters = mutableMapOf<Long, Rewriter>()
|
||||
private var proofreaders = mutableMapOf<Int, Proofreader>()
|
||||
|
||||
private fun rewriterCacheKey(
|
||||
@RewriterOptions.OutputType outputType: Int,
|
||||
@RewriterOptions.Language language: Int,
|
||||
): Long = (outputType.toLong() shl 32) or language.toLong()
|
||||
|
||||
private fun getRewriter(
|
||||
@RewriterOptions.OutputType outputType: Int,
|
||||
@RewriterOptions.Language language: Int,
|
||||
): Rewriter =
|
||||
rewriters.getOrPut(rewriterCacheKey(outputType, language)) {
|
||||
Rewriting.getClient(
|
||||
RewriterOptions
|
||||
.builder(context)
|
||||
.setOutputType(outputType)
|
||||
.setLanguage(language)
|
||||
.build(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getProofreader(
|
||||
@ProofreaderOptions.Language language: Int,
|
||||
): Proofreader =
|
||||
proofreaders.getOrPut(language) {
|
||||
Proofreading.getClient(
|
||||
ProofreaderOptions
|
||||
.builder(context)
|
||||
.setInputType(ProofreaderOptions.InputType.KEYBOARD)
|
||||
.setLanguage(language)
|
||||
.build(),
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun checkAvailability(): WritingAssistantStatus =
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val rewriter = getRewriter(RewriterOptions.OutputType.REPHRASE, RewriterOptions.Language.ENGLISH)
|
||||
val status = rewriter.checkFeatureStatus().get()
|
||||
when (status) {
|
||||
FeatureStatus.AVAILABLE -> WritingAssistantStatus.Available
|
||||
FeatureStatus.DOWNLOADING -> WritingAssistantStatus.Downloading
|
||||
else -> WritingAssistantStatus.Unavailable
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
WritingAssistantStatus.Unavailable
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun transform(
|
||||
text: String,
|
||||
tone: WritingTone,
|
||||
): WritingResult {
|
||||
val language = detectLanguage(text)
|
||||
val transformedText =
|
||||
when (tone) {
|
||||
WritingTone.CORRECT -> proofread(text, language)
|
||||
WritingTone.REPHRASE -> rewrite(text, RewriterOptions.OutputType.REPHRASE, language)
|
||||
WritingTone.SHORTER -> rewrite(text, RewriterOptions.OutputType.SHORTEN, language)
|
||||
WritingTone.ELABORATE -> rewrite(text, RewriterOptions.OutputType.ELABORATE, language)
|
||||
WritingTone.FRIENDLY -> rewrite(text, RewriterOptions.OutputType.FRIENDLY, language)
|
||||
WritingTone.PROFESSIONAL -> rewrite(text, RewriterOptions.OutputType.PROFESSIONAL, language)
|
||||
WritingTone.EMOJIFY -> rewrite(text, RewriterOptions.OutputType.EMOJIFY, language)
|
||||
WritingTone.MORE_DIRECT -> rewrite(text, RewriterOptions.OutputType.PROFESSIONAL, language)
|
||||
WritingTone.PUNCHY -> rewrite(text, RewriterOptions.OutputType.SHORTEN, language)
|
||||
}
|
||||
|
||||
return WritingResult(
|
||||
originalText = text,
|
||||
transformedText = transformedText,
|
||||
tone = tone,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun detectLanguage(text: String): Int =
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val langTag = Tasks.await(LanguageTranslatorService.identifyLanguage(text))
|
||||
mapLanguageTag(langTag)
|
||||
} catch (e: Exception) {
|
||||
RewriterOptions.Language.ENGLISH
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun rewrite(
|
||||
text: String,
|
||||
@RewriterOptions.OutputType outputType: Int,
|
||||
@RewriterOptions.Language language: Int,
|
||||
): String =
|
||||
withContext(Dispatchers.IO) {
|
||||
val rewriter = getRewriter(outputType, language)
|
||||
val request = RewritingRequest.builder(text).build()
|
||||
val result = rewriter.runInference(request).get()
|
||||
result.results.firstOrNull()?.text ?: text
|
||||
}
|
||||
|
||||
private suspend fun proofread(
|
||||
text: String,
|
||||
@ProofreaderOptions.Language language: Int,
|
||||
): String =
|
||||
withContext(Dispatchers.IO) {
|
||||
val proofreader = getProofreader(language)
|
||||
val request = ProofreadingRequest.builder(text).build()
|
||||
val result = proofreader.runInference(request).get()
|
||||
result.results.firstOrNull()?.text ?: text
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
rewriters.values.forEach { it.close() }
|
||||
rewriters.clear()
|
||||
proofreaders.values.forEach { it.close() }
|
||||
proofreaders.clear()
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun mapLanguageTag(tag: String?): Int =
|
||||
when (tag?.lowercase()?.take(2)) {
|
||||
"en" -> RewriterOptions.Language.ENGLISH
|
||||
"ja" -> RewriterOptions.Language.JAPANESE
|
||||
"ko" -> RewriterOptions.Language.KOREAN
|
||||
"de" -> RewriterOptions.Language.GERMAN
|
||||
"fr" -> RewriterOptions.Language.FRENCH
|
||||
"it" -> RewriterOptions.Language.ITALIAN
|
||||
"es" -> RewriterOptions.Language.SPANISH
|
||||
else -> RewriterOptions.Language.ENGLISH
|
||||
}
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.service.ai
|
||||
|
||||
import android.content.Context
|
||||
|
||||
object WritingAssistantFactory {
|
||||
fun create(context: Context): WritingAssistant = MLKitWritingAssistant(context)
|
||||
}
|
||||
@@ -30,6 +30,9 @@ kotlin = "2.3.20"
|
||||
kotlinxCollectionsImmutable = "0.4.0"
|
||||
kotlinxCoroutinesCore = "1.10.2"
|
||||
kotlinxSerialization = "1.10.0"
|
||||
genaiProofreading = "1.0.0-beta1"
|
||||
genaiPrompt = "1.0.0-beta2"
|
||||
genaiRewriting = "1.0.0-beta1"
|
||||
languageId = "17.0.6"
|
||||
lifecycleRuntimeKtx = "2.10.0"
|
||||
lightcompressor-enhanced = "1.8.1"
|
||||
@@ -134,6 +137,9 @@ jetbrains-compose-runtime = { module = "org.jetbrains.compose.runtime:runtime",
|
||||
jetbrains-compose-ui = { module = "org.jetbrains.compose.ui:ui", version.ref = "jetbrainsCompose" }
|
||||
jetbrains-compose-ui-tooling = { module = "org.jetbrains.compose.ui:ui-tooling", version.ref = "jetbrainsCompose" }
|
||||
jetbrains-compose-ui-tooling-preview = { module = "org.jetbrains.compose.ui:ui-tooling-preview", version.ref = "jetbrainsCompose" }
|
||||
google-mlkit-genai-proofreading = { group = "com.google.mlkit", name = "genai-proofreading", version.ref = "genaiProofreading" }
|
||||
google-mlkit-genai-prompt = { group = "com.google.mlkit", name = "genai-prompt", version.ref = "genaiPrompt" }
|
||||
google-mlkit-genai-rewriting = { group = "com.google.mlkit", name = "genai-rewriting", version.ref = "genaiRewriting" }
|
||||
google-mlkit-language-id = { group = "com.google.mlkit", name = "language-id", version.ref = "languageId" }
|
||||
google-mlkit-translate = { group = "com.google.mlkit", name = "translate", version.ref = "translate" }
|
||||
jackson-module-kotlin = { group = "com.fasterxml.jackson.module", name = "jackson-module-kotlin", version.ref = "jacksonModuleKotlin" }
|
||||
|
||||
Reference in New Issue
Block a user