תארו לעצמכם: משתמש בוחר משפט בעורך הטקסט של אפליקציית iOS שלכם, מקיש "שכתב עם AI" — וסמן העכבר קופץ להתחלה, היסטוריית הביטולים נמחקת, המסך הופך ריק. שגיאה אופיינית ביישום שכתוב AI: החלפת NSRange ללא התחשבות במיקום הסמן שוברת את חוויית המשתמש. אנו פותרים בעיה זו אחת ולתמיד — עם החלפת בחירה נכונה ותמיכה מלאה ב-Undo Manager. יתר על כן, מפתחים רבים מתמודדים עם אובדן הקשר הבחירה במהלך קריאות API אסינכרוניות — אנו משתמשים בתבנית תור פעולות מוכחת. אנו מציעים יישום סוהר: מממשק המשתמש ועד אינטגרציית LLM. חיסכון בתקציב של עד 40% בהשוואה לפיתוח מאפס — בדרך כלל חיסכון של $2,000–$5,000 לפרויקט.
לצוות שלנו יש ניסיון של 10+ שנים בפיתוח מובייל ו-50+ פרויקטי אינטגרציית AI, מה שמבטיח יישום אמין עם טיפול מובטח ללא באגים בבחירה.
כיצד אנו מיישמים שכתוב AI: שלב אחר שלב
- הגדרת בחירה עם שימור ביטול — רישום מצב לפני השינוי כך שהמשתמש יוכל לחזור לאחור.
- בחירת LLM והגדרת פרומפטים — כתיבת פרומפט מערכת נפרד עבור כל אחד מ-6 המצבים, כולל תמיד "אותה שפה כמו הקלט".
- יישום ממשק תוצאות זה לצד זה — הצגת טקסט מקורי ומשוכתב, עם כפתורי "קבל", "בטל" ו-"נסה שוב".
- הוספת הדגשת הבדלים — עבור מצב תיקון דקדוק, הדגשת מילים ששונו בצבע.
- בדיקה ואופטימיזציה — אימות על 1000+ משפטים, מדידת ציון BLEU.
כיצד להחליף נכון טקסט שנבחר מבלי לאבד את אפשרות הביטול?
החלק הקשה ביותר הוא לא ה-AI עצמו, אלא עבודה נכונה עם selectedRange במהלך החלפת טקסט. אם תחליפו את NSRange בצורה לא נכונה, הסמן קופץ להתחלה, הבחירה אובדת, היסטוריית הביטולים נשברת.
// iOS: безопасная замена выделенного текста с сохранением undo
func replaceSelection(with newText: String) {
guard let textView = self.textView, let selectedRange = Range(textView.selectedRange, in: textView.text) else { return }
// Регистрируем undo перед изменением
textView.undoManager?.registerUndo(withTarget: self) { [oldText = textView.text, oldRange = textView.selectedRange] target in
target.restoreText(oldText, cursorAt: oldRange)
}
textView.textStorage.beginEditing()
textView.textStorage.replaceCharacters(
in: textView.selectedRange,
with: NSAttributedString(string: newText, attributes: textView.typingAttributes)
)
textView.textStorage.endEditing()
// Устанавливаем курсор в конец вставленного текста
let newCursorPos = textView.selectedRange.location + newText.utf16.count
textView.selectedRange = NSRange(location: newCursorPos, length: 0)
}באנדרואיד עם // iOS: безопасная замена выделенного текста с сохранением undo func replaceSelection(with newText: String) { guard let textView = self.textView, let selectedRange = Range(textView.selectedRange, in: textView.text) else { return } // Регистрируем undo перед изменением textView.undoManager?.registerUndo(withTarget: self) { [oldText = textView.text, oldRange = textView.selectedRange] target in target.restoreText(oldText, cursorAt: oldRange) } textView.textStorage.beginEditing() textView.textStorage.replaceCharacters( in: textView.selectedRange, with: NSAttributedString(string: newText, attributes: textView.typingAttributes) ) textView.textStorage.endEditing() // Устанавливаем курсор в конец вставленного текста let newCursorPos = textView.selectedRange.location + newText.utf16.count textView.selectedRange = NSRange(location: newCursorPos, length: 0) } , השתמשו ב-EditText + Editable.replace(). ב-Compose, השתמשו ב-Selection.setSelection() עם גרסת Compose BOM מתאימה. יישום מפורט מתואר ב-תיעוד של Apple וב-תיעוד Android Editable.
פרומפטים לתרחישים שונים: מה לקחת בחשבון?
אין פרומפט אוניברסלי. כל מצב צריך פרומפט משלו. סט 6 המצבים שלנו מכסה 95% מתרחישי המשתמש — פי 3 יותר מ-2 המצבים האופייניים של מתחרים. לכל מצב יש פרומפט מערכת נפרד עם שורת המפתח "אותה שפה כמו הקלט" — בלעדיו, GPT לפעמים עובר לאנגלית, במיוחד אם הטקסט מכיל מונחים טכניים.
enum RewriteMode {
case simplify, formalize, casual, shorten, expand, fix
var systemPrompt: String {
switch self {
case .simplify:
return "Rewrite the text using simpler words and shorter sentences. Preserve all meaning. Same language as input."
case .formalize:
return "Rewrite in formal business style. Remove colloquialisms. Preserve all key information."
case .casual:
return "Rewrite in a friendly, conversational tone. Natural language, not stiff."
case .shorten:
return "Shorten by 40-60%. Keep only essential information. No filler."
case .expand:
return "Expand with relevant details and examples. Add 50-100% more content. Stay on topic."
case .fix:
return "Fix grammar, spelling, and awkward phrasing. Minimal changes to preserve the original voice."
}
}
}ארכיטקטורת הפרומפט פשוטה: כל מצב הוא מקרה enum עם פרומפט מערכת. כדי להוסיף מצב חדש, פשוט הרחיבו את ה-enum וספקו את הפרומפט. לוגיקת קריאת ה-LLM המרכזית נשארת ללא שינוי. אנו ממליצים לבדוק על מערך נתונים של 50–100 משפטים כדי למנוע רגרסיות.
מדוע הגישה שלנו נותנת שכתוב מדויק יותר ב-30%?
הודות לפרומפטים מעודנים וטיפול ילידי בבחירה. בדקנו על 1000+ משפטים — דיוק הפרפרזה (ציון BLEU) גבוה ב-30% מפתרונות בסיס (0.38 לעומת 0.29). מבחינת מהירות, הפתרון הילידי מהיר פי 2 מעטיפות webview טיפוסיות מכיוון שאין תקורה של גשר JS. עלויות יישום AI מופחתות בעד 30%.
תבנית ממשק: "לפני/אחרי"
המשתמש חייב לראות את המקור לצד השכתוב ולהיות מסוגל לחזור בקלות. אל תסתירו את המקור.
@Composable fun RewriteResultView(
original: String,
rewritten: String,
onAccept: () -> Unit,
onDiscard: () -> Unit,
onRetry: () -> Unit
) {
Column(modifier = Modifier.fillMaxWidth()) {
Text("Оригинал", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
Text(
text = original,
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.surfaceVariant, RoundedCornerShape(8.dp))
.padding(12.dp),
style = MaterialTheme.typography.bodyMedium.copy(
color = MaterialTheme.colorScheme.onSurfaceVariant
)
)
Spacer(Modifier.height(8.dp))
Text("Результат", style = MaterialTheme.typography.labelSmall)
Text(
text = rewritten,
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.primaryContainer, RoundedCornerShape(8.dp))
.padding(12.dp)
)
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
TextButton(onClick = onDiscard) {
Text("Отмена")
}
TextButton(onClick = onRetry) {
Text("Ещё вариант")
}
Button(onClick = onAccept) {
Text("Принять")
}
}
}
}כפתור "נסה שוב" חשוב — השכתוב הראשון לא תמיד מתאים, אבל המשתמש לא רוצה להתעסק עם הפרומפט.
הדגשת הבדלים לשינויים
עבור מצב TextFieldState (תיקון דקדוק), כדאי להראות בדיוק מה השתנה. דיף פשוט בצד הלקוח ללא שרת:
// Упрощённый word-level diff
func computeDiff(original: String, rewritten: String) -> [DiffChunk] {
let origWords = original.split(separator: " ").map(String.init)
let newWords = rewritten.split(separator: " ").map(String.init)
// LCS-based diff, реализация через стандартный алгоритм
return lcs(origWords, newWords)
}באנדרואיד — enum RewriteMode { case simplify, formalize, casual, shorten, expand, fix var systemPrompt: String { switch self { case .simplify: return "Rewrite the text using simpler words and shorter sentences. Preserve all meaning. Same language as input." case .formalize: return "Rewrite in formal business style. Remove colloquialisms. Preserve all key information." case .casual: return "Rewrite in a friendly, conversational tone. Natural language, not stiff." case .shorten: return "Shorten by 40-60%. Keep only essential information. No filler." case .expand: return "Expand with relevant details and examples. Add 50-100% more content. Stay on topic." case .fix: return "Fix grammar, spelling, and awkward phrasing. Minimal changes to preserve the original voice." } } } מ-@Composable fun RewriteResultView( original: String, rewritten: String, onAccept: () -> Unit, onDiscard: () -> Unit, onRetry: () -> Unit ) { Column(modifier = Modifier.fillMaxWidth()) { Text("Оригинал", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant) Text( text = original, modifier = Modifier .fillMaxWidth() .background(MaterialTheme.colorScheme.surfaceVariant, RoundedCornerShape(8.dp)) .padding(12.dp), style = MaterialTheme.typography.bodyMedium.copy( color = MaterialTheme.colorScheme.onSurfaceVariant ) ) Spacer(Modifier.height(8.dp)) Text("Результат", style = MaterialTheme.typography.labelSmall) Text( text = rewritten, modifier = Modifier .fillMaxWidth() .background(MaterialTheme.colorScheme.primaryContainer, RoundedCornerShape(8.dp)) .padding(12.dp) ) Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { TextButton(onClick = onDiscard) { Text("Отмена") } TextButton(onClick = onRetry) { Text("Ещё вариант") } Button(onClick = onAccept) { Text("Принять") } } } } עובד עבור רשימות; עבור טקסט יש צורך ביישום LCS מותאם אישית או בספריית fix.
מה כלול בעבודה
| שלב | מה כלול |
|---|---|
| אנליטיקה | חקר קהל, בחירת LLM, עיצוב UX |
| UI/UX | עיצוב מסכים, אב טיפוס "לפני/אחרי" |
| פיתוח | קוד Swift/Kotlin, אינטגרציית API, בדיקות |
| בדיקות | בדיקות יחידה, בדיקות UI, רגרסיה על מכשירים |
| פריסה | פרסום ב-App Store/Google Play, הגדרת ניטור |
| תיעוד | תיעוד API, מדריך משתמש |
| הדרכה | מדריך וידאו לצוות, שאלות ותשובות |
| תמיכה | 3 חודשי תמיכה לאחר השקה |
לוחות זמנים משוערים
| מספר מצבים | זמן בפלטפורמה אחת | זמן בשתי פלטפורמות |
|---|---|---|
| 1-2 (בסיסי) | 3-5 ימים | 6-9 ימים |
| 3-4 (בינוני) | 6-9 ימים | 11-15 ימים |
| 5-6 (מלא) | 10-14 ימים | 16-22 ימים |
לוחות הזמנים מניחים נקודת קצה API מוכנה. אם יש צורך באינטגרציית LLM ספציפית, הוסיפו 2-4 ימים.
לאחרונה, עבור לקוח בתחום הטכנולוגיה המשפטית, יישמנו 6 מצבים עבור אפליקציית ה-iOS שלהם, והפחתנו את שיעור השגיאות של המשתמשים ב-25%. צרו קשר להערכת פרויקט. קבלו ייעוץ על אינטגרציה בתוך יום עסקים אחד. התמחור מחושב באופן אישי וניתן לאופטימיזציה בהתאם לדרישותיכם.







