feat: precompute all AI tone results in parallel
Instead of computing on-tap, all 9 tones are precomputed in parallel after a 1-second debounce when the user stops typing. Tone chips only appear once results are ready. Tapping a chip instantly shows the precomputed result. Text changes cancel and restart the computation. https://claude.ai/code/session_01RbCYGrbbapRMike8WQy41F
This commit is contained in:
+20
-26
@@ -26,24 +26,20 @@ 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.Box
|
||||
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.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
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.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -55,8 +51,8 @@ import com.vitorpamplona.amethyst.ui.stringRes
|
||||
@Composable
|
||||
fun AiWritingHelpPanel(
|
||||
isVisible: Boolean,
|
||||
isProcessing: Boolean,
|
||||
result: WritingResult?,
|
||||
readyResults: Map<WritingTone, WritingResult>,
|
||||
selectedResult: WritingResult?,
|
||||
onToneSelected: (WritingTone) -> Unit,
|
||||
onApply: () -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
@@ -72,19 +68,7 @@ fun AiWritingHelpPanel(
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 8.dp),
|
||||
) {
|
||||
if (isProcessing) {
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 12.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
CircularProgressIndicator(modifier = Modifier.size(24.dp))
|
||||
}
|
||||
}
|
||||
|
||||
result?.let {
|
||||
selectedResult?.let {
|
||||
AiResultCard(
|
||||
result = it,
|
||||
onApply = onApply,
|
||||
@@ -92,7 +76,11 @@ fun AiWritingHelpPanel(
|
||||
)
|
||||
}
|
||||
|
||||
ToneChipRow(onToneSelected = onToneSelected)
|
||||
ToneChipRow(
|
||||
readyTones = readyResults.keys,
|
||||
selectedTone = selectedResult?.tone,
|
||||
onToneSelected = onToneSelected,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -141,7 +129,11 @@ private fun AiResultCard(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ToneChipRow(onToneSelected: (WritingTone) -> Unit) {
|
||||
private fun ToneChipRow(
|
||||
readyTones: Set<WritingTone>,
|
||||
selectedTone: WritingTone?,
|
||||
onToneSelected: (WritingTone) -> Unit,
|
||||
) {
|
||||
val scrollState = rememberScrollState()
|
||||
Row(
|
||||
modifier =
|
||||
@@ -152,11 +144,13 @@ private fun ToneChipRow(onToneSelected: (WritingTone) -> Unit) {
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
) {
|
||||
WritingTone.entries.forEach { tone ->
|
||||
FilterChip(
|
||||
selected = false,
|
||||
onClick = { onToneSelected(tone) },
|
||||
label = { Text(toneDisplayName(tone)) },
|
||||
)
|
||||
if (tone in readyTones) {
|
||||
FilterChip(
|
||||
selected = tone == selectedTone,
|
||||
onClick = { onToneSelected(tone) },
|
||||
label = { Text(toneDisplayName(tone)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -563,9 +563,9 @@ private fun NewPostScreenBody(
|
||||
|
||||
AiWritingHelpPanel(
|
||||
isVisible = postViewModel.showAiPanel,
|
||||
isProcessing = postViewModel.isAiProcessing,
|
||||
result = postViewModel.aiResult,
|
||||
onToneSelected = postViewModel::requestAiTransform,
|
||||
readyResults = postViewModel.aiResults,
|
||||
selectedResult = postViewModel.aiSelectedResult,
|
||||
onToneSelected = postViewModel::selectAiResult,
|
||||
onApply = postViewModel::applyAiResult,
|
||||
onDismiss = postViewModel::dismissAiResult,
|
||||
)
|
||||
|
||||
+55
-21
@@ -147,7 +147,12 @@ 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.coroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -302,17 +307,19 @@ open class ShortNotePostViewModel :
|
||||
// TODO: Remove useMockAi before shipping. Set to true to test UI without Gemini Nano.
|
||||
private val useMockAi = true
|
||||
|
||||
var aiResult by mutableStateOf<WritingResult?>(null)
|
||||
var aiResults by mutableStateOf<Map<WritingTone, WritingResult>>(emptyMap())
|
||||
var aiSelectedResult by mutableStateOf<WritingResult?>(null)
|
||||
var aiStatus by mutableStateOf<WritingAssistantStatus>(WritingAssistantStatus.Unavailable)
|
||||
var isAiProcessing by mutableStateOf(false)
|
||||
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
|
||||
return prefEnabled && aiStatus is WritingAssistantStatus.Available && aiResults.isNotEmpty()
|
||||
}
|
||||
|
||||
fun initWritingAssistant(context: android.content.Context) {
|
||||
@@ -329,35 +336,61 @@ open class ShortNotePostViewModel :
|
||||
}
|
||||
}
|
||||
|
||||
fun requestAiTransform(tone: WritingTone) {
|
||||
val text = message.text.toString()
|
||||
if (text.isBlank() || isAiProcessing) return
|
||||
fun precomputeAiResults() {
|
||||
val assistant = writingAssistant ?: return
|
||||
if (aiStatus !is WritingAssistantStatus.Available) return
|
||||
val prefEnabled =
|
||||
accountViewModel.settings.uiSettingsFlow.automaticallyProposeAiImprovements.value ==
|
||||
BooleanType.ALWAYS
|
||||
if (!prefEnabled) return
|
||||
|
||||
isAiProcessing = true
|
||||
aiResult = null
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val result = writingAssistant?.transform(text, tone)
|
||||
aiResult = result
|
||||
} catch (e: Exception) {
|
||||
if (e is kotlinx.coroutines.CancellationException) throw e
|
||||
aiResult = null
|
||||
} finally {
|
||||
isAiProcessing = false
|
||||
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
|
||||
|
||||
coroutineScope {
|
||||
WritingTone.entries
|
||||
.map { tone ->
|
||||
async {
|
||||
try {
|
||||
assistant.transform(text, tone)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
null
|
||||
}
|
||||
}
|
||||
}.forEach { deferred ->
|
||||
val result = deferred.await() ?: return@forEach
|
||||
aiResults = aiResults + (result.tone to result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun selectAiResult(tone: WritingTone) {
|
||||
aiSelectedResult = aiResults[tone]
|
||||
}
|
||||
|
||||
fun applyAiResult() {
|
||||
aiResult?.let {
|
||||
aiSelectedResult?.let {
|
||||
message.setTextAndPlaceCursorAtEnd(it.transformedText)
|
||||
aiResult = null
|
||||
aiSelectedResult = null
|
||||
aiResults = emptyMap()
|
||||
lastComputedText = ""
|
||||
draftTag.newVersion()
|
||||
}
|
||||
}
|
||||
|
||||
fun dismissAiResult() {
|
||||
aiResult = null
|
||||
aiSelectedResult = null
|
||||
}
|
||||
|
||||
fun lnAddress(): String? = account.userProfile().lnAddress()
|
||||
@@ -1200,6 +1233,7 @@ open class ShortNotePostViewModel :
|
||||
emojiSuggestions?.processCurrentWord(lastWord)
|
||||
}
|
||||
|
||||
precomputeAiResults()
|
||||
draftTag.newVersion()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user