במאמר הזה מוסבר איך לשלב ולהתאים אישית את Headless Mobile SDK באפליקציה ל-Android. הוא כולל את הפעולות החכמות, הקבצים המצורפים וההפניה.
הגדרת פעולות חכמות וקבצים מצורפים
פעולות חכמות מאפשרות לסוכנים וללקוחות להחליף מידע עשיר ומובנה, כמו תמונות, סרטונים, קטעי טקסט ואימות ביומטרי.
בארכיטקטורה ללא ממשק משתמש, ערכת ה-SDK פועלת אך ורק כשכבת התעבורה המאובטחת. היא מודיעה לכם מתי נדרשת או מתבקשת פעולה, ומעבירה את הנתונים בצורה מאובטחת לפלטפורמת CCAI Platform. מכיוון שממשק המשתמש הוא בבעלותכם, האפליקציה שלכם אחראית באופן מלא ליצירת מסכי המצלמה, כלי בחירת התמונות, תיבות הטקסט והפעלת הפרומפטים הביומטריים המובנים של Android.
פעולות חכמות לפני הפגישה
פעולות לפני סשן מאפשרות לאסוף נתונים (כמו מספר חשבון או סרטון של מוצר מקולקל) לפני שהמשתמש מצטרף לתור, וכך לקצר משמעותית את זמן הטיפול של הנציג.
מה ה-SDK מספק: הכללים והדרישות לתור ספציפי (לדוגמה, האם אימות ביומטרי, העלאת תמונה או הזנת טקסט הם אופציונליים או חובה לפני ההצטרפות).
מה יוצרים: מסכי ממשק המשתמש לפני הסשן (מצלמה, הזנת טקסט והנחיות ביומטריות) שמופיעים בין הרגע שבו המשתמש מקיש על התור לבין הרגע שבו הוא נכנס לחדר ההמתנה, וגם הלוגיקה של שמירת נתונים במטמון בדיסק כדי לשמור בבטחה את המדיה עד שהסשן מתחבר.
דוגמה להטמעה
הקוד הזה בודק את ההגדרה של ה-SDK כדי לראות אם לתור שנבחר יש דרישות לפני הפגישה. הוא מדגים איך לטפל בדרישה של סרטון על ידי שמירת הפניה לקובץ Uri בדיסק במקום שמירת הסרטון בזיכרון.
// 1. Check requirements and collect data BEFORE joining the queue
fun checkPreSessionRequirements(queueMenu: QueueMenu) {
val smartActions = queueMenu.settings
if (smartActions.isNullOrEmpty()) {
startChat(queueMenu)
return
}
// Example: handle a video requirement
presentVideoPicker { localFileUri ->
// IMPORTANT: localFileUri should point to context.cacheDir
MySessionStateManager.pendingPreSessionVideo = localFileUri
startChat(queueMenu)
}
}
// 2. Upload and clean up AFTER the session officially connects
fun handleSessionConnected() {
val pendingVideoUri = MySessionStateManager.pendingPreSessionVideo ?: return
// Upload using the standard in-session pipeline
// then clean up the temporary file
val file = File(pendingVideoUri.path ?: return)
file.delete()
MySessionStateManager.pendingPreSessionVideo = null
}
קריאת הגדרות של פעולות חכמות לפני סשן
ה-SDK חושף הגדרת פעולות חכמות לפני הפעלה דרך שני נתיבים משלימים. כדאי לבדוק את שניהם כדי לקבוע אם נדרש תיעוד לפני תחילת הסשן בתור או בערוץ מסוימים.
נתיב א' – ברמת התור (QueueMenu.settings[].sdk.preSessionSmartAction)
// `QueueMenuSetting` and `QueueMenuSDKSetting` (simplified)
data class QueueMenuSetting(
val sdk: QueueMenuSDKSetting?
// ... other fields ...
)
data class QueueMenuSDKSetting(
val preSessionSmartAction: Boolean?
// ... other fields ...
)
// Usage - "does any queue-level setting require a pre-session smart action?"
fun queueRequiresPreSession(menu: QueueMenu): Boolean {
return menu.settings?.any { it.sdk?.preSessionSmartAction == true } ?: false
}
נתיב ב' – ברמת הערוץ (VoiceCallChannel.preSessionSmartAction)
// Usage - "does the voice channel specifically require pre-session capture?"
val requiresPreSessionOnVoice =
menu.channels.voiceCall?.preSessionSmartAction == true
משתמשים בנתיב א' כדי להחליט אם להציג שער לפני הפעלת התוכן לפני שהמשתמש בוחר ערוץ. משתמשים בנתיב ב' לשיפורים ספציפיים לערוץ אחרי בחירת ערוץ (לדוגמה, הצגת הנחיה רק לפני שיחת טלפון מיידית).
פעולות חכמות במהלך סשן (שהסוכן יזם)
במהלך שיחה פעילה בצ'אט או שיחה קולית, נציג יכול ללחוץ על לחצן בלוח הבקרה שלו כדי לבקש נתונים מהמשתמש (לדוגמה, שליחת תמונה של תעודה מזהה או אימות באמצעות נתונים ביומטריים).
מה ה-SDK מספק: התראות על אירועים בזמן אמת, שמודיעות לאפליקציה שהסוכן ביקש סוג מסוים של פעולה חכמה.
מה מפתחים: התראה בשיחה או בצ'אט שמיידעת את המשתמש על הבקשה, מפעילה את הכלי המובנה המתאים (מצלמה, ספריית תמונות, שדה טקסט או BiometricPrompt של Android) ומעבירה את התוצאה בחזרה ל-SDK.
דוגמה להטמעה
כאן האפליקציה מאזינה לאירועים של ה-SDK. כשהוא מזהה בקשה לסוכן, הוא מציג התראה ואז פותח את הכלי המתאים של Android.
import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat
// Listen for Smart Action requests from the active SDK session.
//
// `SmartActionRequest` exposes a nested `smartAction: SmartAction`, and the
// switchable type lives at `request.smartAction.type`. The `SmartActionType`
// enum is `PascalCase: Unknown`, `Verification`, `Screenshot`, `Photo`, `Video`,
// `TextInput`, `ScreenShare`. Use `Verification` for Face ID / fingerprint flows.
fun handleSmartActionRequest(request: SmartActionRequest) {
when (request.smartAction.type) {
SmartActionType.Photo -> {
promptUserForPhoto(message = "The agent requested a photo.") { capturedBitmap ->
// Send the image back to the agent using the SDK.
sendAttachment(image = capturedBitmap)
}
}
SmartActionType.Verification -> {
val executor = ContextCompat.getMainExecutor(this)
val biometricPrompt = BiometricPrompt(this, executor,
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(
result: BiometricPrompt.AuthenticationResult
) {
sendBiometricResult(success = true)
}
override fun onAuthenticationFailed() {
sendBiometricResult(success = false)
}
override fun onAuthenticationError(
errorCode: Int, errString: CharSequence
) {
sendBiometricResult(success = false)
}
}
)
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle("Identity Verification")
.setSubtitle("Verify your identity to continue")
.setNegativeBtnText("Cancel")
.build()
biometricPrompt.authenticate(promptInfo)
}
SmartActionType.TextInput -> {
presentTextInputPrompt(message = "The agent requested text input.") { text ->
sendSmartActionText(text)
}
}
SmartActionType.Video -> {
// Agent-requested video capture. Reuse the same disk-cached file
// pattern recommended for pre-session media keep the recording on
// disk and pass the local Uri to the SDK rather than holding a
// large `ByteArray` in memory.
promptUserForVideo(message = "The agent requested a video.") { capturedVideoUri ->
sendAttachment(videoUri = capturedVideoUri)
}
}
SmartActionType.Screenshot -> {
// The SDK exposes `Screenshot` as a distinct request type the
// user captures (or selects), a single still image of the current
// screen. This is **not** the same as `ScreenShare`, which
// starts a live co-browse session.
promptUserForScreenshot(message = "The agent requested a screenshot.") { capturedBitmap ->
sendAttachment(image = capturedBitmap)
}
}
SmartActionType.ScreenShare -> {
// Hand off to the full screen share flow (see Headless Mobile SDK for Android: Voice, screen share, scheduled call, and email).
// Don't confuse with `Screenshot`; `ScreenShare` starts a
// live co-browse session using `CCAIScreenShare`, while `Screenshot`
// captures a single still frame.
startScreenShareFlow()
}
SmartActionType.Unknown -> {
// Future-proof: a smart action type the SDK does not recognize.
// Log and continue; don't block the user.
Log.w("CCAI", "Received unknown smart action type")
}
}
}
פעולות חכמות שהסוכן יזם מגיעות דרך אירועי ה-SDK של הסשן הפעיל. כדי שהאפליקציה תוכל להציג את ההנחיה המתאימה ללקוח ברגע שמגיעה בקשה, צריך להירשם לאירועים הרלוונטיים של הצ'אט או השיחה לפני שמתחילים את הסשן.
התראות ומעקב אחרי פעולות חכמות
אפשר להשתמש ב-smartActionService כדי לאשר ולעדכן פעולות חכמות כשהמשתמש מתקדם בהן.
val router = CommunicationRouter(
id = chatId,
type = CommunicationType.Chat
)
// Notify the server that a smart action has been received.
lifecycleScope.launch {
try {
CCAI.smartActionService?.notifySmartActionReceived(
router = router,
smartActionId = smartActionId
)
Log.d("CCAI", "Smart action notification sent successfully")
} catch (e: Exception) {
Log.e("CCAI", "Failed to notify smart action: ${e.message}")
}
}
// Update the smart action status as the user completes it.
lifecycleScope.launch {
try {
CCAI.smartActionService?.updateSmartActionStatus(
router = router,
smartActionId = smartActionId,
status = SmartActionStatus.Finished
)
Log.d("CCAI", "Smart action status updated successfully")
} catch (e: Exception) {
Log.e("CCAI", "Failed to update smart action status: ${e.message}")
}
}
אם הפעולה דורשת קלט טקסט, צריך לאסוף את הטקסט בממשק המשתמש ולשלוח אותו דרך שירות הפעולות החכמות לפני שמסמנים את הפעולה כהושלמה.
טפסים באינטרנט
מה ערכת ה-SDK מספקת: ממשקי API לאחזור, להצגה ולבדיקה של טפסים באינטרנט שמופעלים על ידי נציגים במהלך סשן צ'אט.
מה יוצרים: ממשק משתמש לעיבוד טופס האינטרנט ותהליך השליחה שכולל את התשובות של המשתמש.
דוגמה להטמעה
הקוד הזה מאחזר טופס אינטרנט לפי המזהה החיצוני ומזהה הפעולה החכמה שלו, ואז מאמת את התשובה לפני השליחה.
// Fetch a web form - `fetchWebForm` takes two positional parameters on Android.
lifecycleScope.launch {
try {
val webFormResponse = CCAI.chatService?.fetchWebForm(
externalFormId = "form123",
smartActionId = 123
)
if (webFormResponse != null) {
Log.d("CCAI", "Web form received: $webFormResponse")
// Render the web form in your custom UI.
renderWebForm(webFormResponse)
}
} catch (e: Exception) {
Log.e("CCAI", "Failed to fetch web form: ${e.message}")
}
}
// Validate a web form response.
lifecycleScope.launch {
try {
val isValid = CCAI.chatService?.validateWebForm(webFormResponse)
if (isValid == true) {
Log.d("CCAI", "Web form is valid")
} else {
Log.d("CCAI", "Web form validation failed")
}
} catch (e: Exception) {
Log.e("CCAI", "Failed to validate web form: ${e.message}")
}
}
טפסים בהתאמה אישית
מה ה-SDK מספק: ממשקי API לאחזור הגדרות של טפסים שאפשר להגדיר (עם כמה סוגים של שאלות) ולשליחת התשובות של המשתמשים.
מה יוצרים: ממשק משתמש דינמי של טופס שמציג כל סוג של שאלה (שדות טקסט, תפריטים נפתחים וכו'), מאמת את הקלט ואוסף תשובות לשליחה.
דוגמה להטמעה
הקוד הזה מאחזר הגדרות של שאלות בטופס מותאם אישית, ואז אורז ושולח את התשובות של המשתמש.
// Retrieve custom form details
lifecycleScope.launch {
try {
val formDetails = CCAI.chatService?.getCustomFormDetails(formId = 123)
if (formDetails != null) {
Log.d("CCAI", "Form Title: ${formDetails.title ?: "No title"}")
Log.d("CCAI", "Questions: ${formDetails.questions.size}")
// Render the form dynamically based on question types
renderCustomForm(formDetails)
}
} catch (e: Exception) {
Log.e("CCAI", "Failed to get custom form details: ${e.message}")
}
}
// Submit a custom form
val request = SubmitCustomFormRequest(
smartActionId = 123,
formResponse = listOf(
SubmitCustomFormRequest.Answer(questionId = 1, value = "John Doe"),
SubmitCustomFormRequest.Answer(questionId = 2, value = "john@example.com")
)
)
lifecycleScope.launch {
try {
CCAI.chatService?.submitCustomForm(request = request)
Log.d("CCAI", "Form submitted successfully")
} catch (e: Exception) {
Log.e("CCAI", "Failed to submit form: ${e.message}")
}
}
הגדרות מלאות של סוגי ה-enum CustomFormDetailsResponse, CustomFormQuestion,
CustomFormQuestionOption ו-CustomFormQuestionType זמינות במאמר Headless Mobile SDK — Android API Reference.
קבצים מצורפים ביוזמת הצרכן (צ'אט)
המשתמשים לא צריכים לחכות שהנציג יבקש, הם יכולים לשלוח תמונות וסרטונים באופן יזום במהלך הצ'אט. ה-SDK אוכף מגבלות קפדניות על גודל הקובץ ומנסה לדחוס אותו באופן אוטומטי לפני ההעברה.
מגבלות הגודל של קבצים מצורפים
תמונות: עד 2MB
סרטונים: עד 20MB
מה ה-SDK מספק: צינור העלאה מאובטח שמטפל בהעברה ברשת ודוחס קבצים באופן אוטומטי כדי לעמוד במגבלות של פלטפורמת CCAI.
מה אתם בונים: לחצן האטב / הקובץ המצורף בממשק המשתמש של הצ'אט, הכלי לבחירת תמונות / סרטונים ובועת הצ'אט החזותית שמציגה את סטטוס ההעלאה (טעינה, נשלח או נכשל).
דוגמה להטמעה
כשמשתמש בוחר תמונה או סרטון מהגלריה שלו, מעבירים את הקובץ המקומי Uri ל-SDK באמצעות אותו API sendMessage שמשמש להעברת כל ההודעות היוצאות. מידע נוסף מופיע במאמר בנושא שליחת הודעות.
ה-SDK מטפל בדחיסה ובהעלאה.
// Called when the user picks an image from their photo gallery
fun userDidSelectImage(imageUri: Uri) {
val messageBubble = myChatAdapter.addLoadingImageBubble()
lifecycleScope.launch {
try {
CCAI.chatService?.sendMessage(
OutgoingMessageContent.Photos(
images = listOf(imageUri),
smartAction = null
)
)
messageBubble.markAsSent()
} catch (e: Exception) {
messageBubble.showError("Failed to send: ${e.message}")
}
}
}
קבצים מצורפים שנשלחו על ידי נציג (צ'אט)
סוכנים יכולים לשלוח קבצים בחזרה למשתמש, כולל תמונות, סרטונים, קטעי אודיו ומסמכים (קובצי PDF, קובצי CSV וכו').
סוגי קבצים נתמכים
פלטפורמת CCAI מקבלת 19 סוגי קבצים בארבע קטגוריות. ממשק המשתמש המותאם אישית צריך לטפל בכל קטגוריה בצורה חלקה (תצוגה מקדימה של תמונה, נגן וידאו, נגן אודיו, הורדת מסמך).
| קטגוריה | סוגים נתמכים |
|---|---|
| תמונות | JPEG, PNG, GIF ו-WebP. |
| וידאו | MP4, MOV, AVI, WMV ו-WebM. |
| אודיו | MP3, WAV, M4A ו-WEBA. |
| מסמכים | PDF, DOC, XLS, PPT, CSV ו-TXT. |
מה ה-SDK מספק: מטען ייעודי (payload) של הודעות צ'אט נכנסות שמועבר באמצעות הודעת שירות הצ'אט Flow שכוללת מטא-נתונים של קבצים מצורפים – ראו Headless Mobile SDK for Android: Advanced topics, localization, and post-session flows. שירות הצ'אט מספק גם downloadMedia
אפשרות לאחזור מאובטח של מדיה שנשלחה על ידי נציגים.
מה שאתם יוצרים: ממשק המשתמש להצגת הקבצים המצורפים בפיד הצ'אט. האפליקציה שלכם מחליטה אם להציג תצוגות מקדימות של תמונות, תמונות ממוזערות של סרטונים, אמצעי בקרה להפעלת אודיו או לחצנים להורדת מסמכים.
דוגמה להטמעה
התבוננות בהודעות נכנסות באמצעות הודעת Flow של שירות הצ'אט. כשמופיעה הודעה חדשה שכוללת מטא-נתונים של מדיה, צריך להפעיל את downloadMedia
לפני העיבוד או הפתיחה של הקובץ.
downloadMedia מחזירה DownloadMediaResponse { data: ByteArray, fileName:
String? } – הבייטים שהורדו במלואם בתוספת שם קובץ אופציונלי לתצוגה. ה-SDK לא שומר הורדות במטמון. אם ממשק המשתמש חוזר לבועה ישנה יותר או חוזר לתצוגת הצ'אט, האפליקציה אחראית לשמירת התמונה המפוענחת במטמון או לשמירת הבייטים. קריאה חוזרת ל-downloadMedia מפעילה אחזור נוסף מהרשת.
import android.graphics.BitmapFactory
import java.io.File
lifecycleScope.launch {
CCAI.chatService?.messagesReceived?.collect { messages ->
for (message in messages) {
renderIncomingMessage(message)
}
}
}
fun renderIncomingMessage(message: ChatMessage) {
val media = message.media
if (media == null) {
myChatAdapter.insertTextBubble(text = message.text)
return
}
lifecycleScope.launch {
try {
// `downloadMedia` returns the raw bytes plus an optional filename.
val response = CCAI.chatService?.downloadMedia(
mediaId = media.id,
mediaType = media.type
) ?: return@launch
when (media.type) {
DownloadableMediaType.Photo -> {
// Decode in-memory; no disk write needed.
val bitmap = BitmapFactory.decodeByteArray(
response.data, 0, response.data.size
)
myChatAdapter.insertImageBubble(
bitmap = bitmap,
fileName = response.fileName
)
}
DownloadableMediaType.Video,
DownloadableMediaType.Audio,
DownloadableMediaType.Document -> {
// Larger media usually needs an on-disk file for a player
// or document viewer. Write to the app's cache directory
// yourself and hand the resulting File/Uri to your UI.
val file = File(
applicationContext.cacheDir,
response.fileName ?: "attachment"
)
file.writeBytes(response.data)
myChatAdapter.insertAttachmentBubble(
file = file,
mediaType = media.type
)
}
}
} catch (e: Exception) {
myChatAdapter.insertAttachmentErrorBubble(fileName = message.media?.name)
}
}
}
הפניה לטיפול עצמי (לאחר שעות הפעילות ובמקרים של עומס יתר)
הפניית שיחות היא הפניה אוטומטית של משתמשים כשהצוות שלכם לא זמין. בדרך כלל זה קורה באחד משני תרחישים:
מחוץ לשעות הפעילות: המשתמש מנסה ליצור קשר עם התמיכה מחוץ לשעות הפעילות שהוגדרו.
מעל הקיבולת: התור פתוח, אבל זמן ההמתנה ארוך יותר מהסף שהוגדר על ידי האדמין.
בארכיטקטורה ללא ראש, ה-SDK לא חוסם את המשתמש באופן אוטומטי ולא מציג מסך שגיאה. במקום זאת, הוא מעביר בשקט לאפליקציה את סטטוס ההפניה ואת אפשרויות הגיבוי שהוגדרו על ידי האדמין. האפליקציה צריכה לבדוק את הסטטוס הזה לפני שהיא מנסה להתחיל סשן.
טיפול במצבי הפניה
מה ה-SDK מספק: deflectionFrom: List<QueueMenuDeflection>? ב-QueueMenu שמפרט את כללי ההפניה הנכנסים להפניית תורים אחרים לתור הזה. כל QueueMenuDeflection מכיל רק שני שדות:
deflectionType: DeflectionType?: הקטגוריה של ההפניה (לדוגמה, אחרי שעות הפעילות או מעבר לקיבולת).
menuId: Int?: המזהה של תור המקור שממנו מועברות הפניות לתור הזה.
QueueMenuDeflection לא כולל הודעה מוצגת או תווית שקריאה לבני אדם. לגבי העותק שמוצג למשתמשים (לדוגמה, המשרד שלנו סגור),צריך להשתמש ב-afterHoursMessage של התור (שנובע מ-QueueMenu.settings[].afterHours) או במחרוזות שהאדמין הגדיר. כדי לראות את השם המוצג של תור היעד שפוענח, מחפשים אותו בעץ התפריטים שכבר אוחזר על ידי menuId.
מה בונים: הלוגיקה לבדיקת מצבי ההפניה האלה לפני שמכניסים משתמש לתור, וממשק המשתמש המותאם אישית (כמו התראה במסך מלא או באנר) שמציג את ההודעה המתאימה ואת לחצני הניתוב החלופיים.
דוגמה להטמעה
הקוד הבא מיירט ניסיון של משתמש להתחיל צ'אט. הוא בודק את המטא-נתונים של ההפניה ב-SDK ב-QueueMenu, ואם כלל פעיל, הוא מציג מסך הפניה באמצעות התור afterHoursMessage (או חלופה).
fun onSupportMenuTapped(queueMenu: QueueMenu) {
// 1. Check if any deflection rules route into this queue.
val deflections = queueMenu.deflectionFrom
if (!deflections.isNullOrEmpty()) {
// Deflection is active - stop routing. Show the custom deflection screen instead.
val message = queueMenu.afterHoursMessage?.text
?: "Support is currently unavailable."
showDeflectionScreen(
message = message,
deflections = deflections
)
} else {
// No deflection rules apply. Safe to proceed to channel selection or directly to chat/call.
startSupportFlow(queueMenu)
}
}
אפשרויות להפניית עיבוד
כשכלל להפניית שיחות פעיל, האדמינים שלכם כנראה יגדירו דרכים חלופיות למשתמש לקבל עזרה – בדרך כלל על ידי הפניה לתור אחר (שאפשר לפתור באמצעות menuId) או על ידי הצגת afterHoursMessage המוגדרת של התור הנוכחי.
מה ה-SDK מספק: רשימת הערכים של QueueMenuDeflection ב-deflectionFrom, כל אחד עם deflectionType ו-menuId.
מה יוצרים: תפריט דינמי שחוזר על הרשומות האלה, מפענח כל menuId מול עץ התפריט שכבר אוחזר כדי לקבל שם לתצוגה, ויוצר את הלחצנים המתאימים.
דוגמה להטמעה
הקוד הזה לוקח את רשומות ההפניה ובונה מסך באופן דינמי. התווית של הלחצן היא השם של תור המקור (שנפתר מתוך menuId), ואם השם לא זמין, התווית היא deflectionType.
fun showDeflectionScreen(message: String, deflections: List<QueueMenuDeflection>) {
// 1. Update your UI with the admin's configured message (or your fallback).
deflectionTitleTextView.text = message
// 2. Loop through the incoming deflection rules and generate buttons.
for (deflection in deflections) {
val sourceMenu = deflection.menuId?.let { findMenuById(it) }
val label = sourceMenu?.name
?: deflection.deflectionType?.name
?: "Alternative option"
val button = MaterialButton(this).apply {
text = label
setOnClickListener {
handleDeflectionAction(deflection)
}
}
menuLinearLayout.addView(button)
}
// 3. Present the compiled screen to the user.
deflectionContainer.isVisible = true
}
// Walk your previously fetched menu tree (for example, by using `QueueMenu.flatten()`) to resolve a `menuId`.
private fun findMenuById(id: Int): QueueMenu? =
cachedRootMenus.flatMap { it.flatten() }.firstOrNull { it.id == id }
deflectionFrom היא רשימת כללי ההפניה האוטומטית שמובילים לתפריט הזה –
כל רשומה menuId מזהה תור מקור שמפנה אוטומטית לתור הנוכחי. האפליקציה קובעת איך לחשוף את הנתונים האלה. בחלק מהשילובים, תור היעד מוצג כהודעה כאן נשלח אותך במקום זאת, ובשילובים אחרים, נוכחות של כלל כלשהו משמשת כשער להצגת מסך כללי עם ההודעה נסה שוב מאוחר יותר.