Реалізація AI-асистента для написання текстів (Writing Assistant) в мобільному додатку
Writing Assistant у мобільному додатку — це не просто поле введення з кнопкою «поліпшити». Це редактор, який працює в контексті: розуміє тип документа (лист, пост, звіт), підтримує стрімінг, не скидає курсор при вставці, та пережує фоновий режим без втрати стану.
Саме на цих деталях ламаються 80% реалізацій.
Архітектура редактора з AI
Перший вибір — використовувати нативний UITextView/EditText або кастомний редактор. Для більшості випадків нативний компонент — правильний вибір, але з AI-функціями з'являється нетривіальне завдання: вставка сгенерованого тексту без руйнування позиції курсору та виділення.
// iOS: вставка AI-тексту через NSTextStorage без скидання курсору
func insertAIText(_ text: String, at range: NSRange) {
guard let textView = self.textView else { return }
let storage = textView.textStorage
// Зберігаємо позицію курсору
let cursorOffset = textView.selectedRange.location
storage.beginEditing()
storage.replaceCharacters(
in: range,
with: NSAttributedString(string: text, attributes: defaultTypingAttributes)
)
storage.endEditing()
// Відновлюємо курсор після вставки
let newOffset = cursorOffset + (text.count - range.length)
textView.selectedRange = NSRange(location: max(0, newOffset), length: 0)
}
На Android аналог через Editable.replace() + збереження SelectionStart/SelectionEnd через android.text.Selection.
Стрімінг та прогресивна вставка
Writing Assistant обов'язково мусить стримити текст — користувач бачить, як AI пише. Технічно це AsyncStream<String> (iOS) або Flow<String> (Android), кожний чанк додається до кінця активного параграфа.
Типова проблема: при швидкому стрімінгу (> 20 символів/сек) UITextView починає лагати на довгих текстах. Причина — textStorage тригерить layout pass на кожній зміні. Рішення — батчинг апдейтів:
private var streamBuffer = ""
private var streamTimer: Timer?
func appendStreamChunk(_ chunk: String) {
streamBuffer += chunk
if streamTimer == nil {
streamTimer = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: false) { [weak self] _ in
guard let self else { return }
self.textView.textStorage.beginEditing()
self.textView.textStorage.append(NSAttributedString(string: self.streamBuffer))
self.textView.textStorage.endEditing()
self.streamBuffer = ""
self.streamTimer = nil
}
}
}
Кожні 50 мс — один layout pass замість 20. На iPhone SE 2nd gen різниця видна невеликим оком.
Режими асистента: як не захламити UI
Writing Assistant зазвичай пропонує кілька дій: продовжити текст, переписати виділене, змінити тон, скоротити, розширити. Якщо показувати всі кнопки одразу — UI перетворюється на хаос.
Правильна схема: контекстне меню з'являється тільки при наявності виділення (для «переписати», «змінити тон»), floating action button з'являється у кінці параграфа (для «продовжити»). Два різних триггери — два різних UX-паттерни.
// Android Compose - floating assistant button
@Composable
fun WritingAssistantOverlay(
textFieldState: TextFieldState,
onContinue: () -> Unit,
onRewrite: (String) -> Unit
) {
val hasSelection = textFieldState.selection.length > 0
AnimatedVisibility(visible = !hasSelection) {
FloatingActionButton(
onClick = onContinue,
modifier = Modifier.align(Alignment.BottomEnd)
) {
Icon(Icons.Default.AutoAwesome, "Продовжити")
}
}
AnimatedVisibility(visible = hasSelection) {
ContextualMenu(
items = listOf("Переписати", "Змінити тон", "Скоротити"),
onSelect = { action ->
val selected = textFieldState.text.substring(textFieldState.selection)
onRewrite("$action: $selected")
}
)
}
}
Промпти для різних дій
Кожна дія асистента — окремий промпт. їх не можна узагальнити в один універсальний. Кілька робочих шаблонів:
Продовження тексту:
Continue the following text naturally, maintaining the same style, language, and tone.
Write 1-3 sentences only. Do not repeat what was already written.
Text: {last_500_chars}
Переписати виділене:
Rewrite the following text. Keep the core meaning but improve clarity and flow.
Language: {detected_language}. Style: {business|casual|formal}.
Text: {selected_text}
Змінити тон:
Rewrite this text in a {formal|casual|empathetic|assertive} tone.
Preserve all key information. Output only the rewritten text.
Text: {selected_text}
Визначення мови — через NLLanguageRecognizer (iOS) або TextClassifier з ML Kit (Android). Не покладайтеся на Locale.current — користувач може писати на іншій мові.
Стан при фоновому режимі
Якщо користувач згорнув додаток у процесі генерації, iOS відправить задачу в URLSession з background конфігурацією або просто скасує запрос. Потрібно зберегти промпт та статус у UserDefaults/SharedPreferences та відновити при поверненні.
При довгих генераціях (> 15 секунд для великих текстів) переходимо на Background Tasks API на iOS або WorkManager на Android — стрімінг у фоні неможливий, але можна отримати кінцевий результат через push notification.
Орієнтири за часом
Базовий асистент з кнопкою «поліпшити» через OpenAI — 3–5 днів. Повноцінний редактор зі стрімінгом, контекстним меню, режимами та збереженням стану — 3–4 тижні. Підтримка офлайн-режиму з on-device моделлю (через CoreML/TFLite) — окремо від 2 тижнів.







