Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
c372cf1
Add: Use latest fluxC version to bring in JetpackAIStore.
hafizrahman Jun 15, 2023
b7d8cf6
Add: AI Repository class.
hafizrahman Jun 15, 2023
6eb314c
Add: AI Prompts class.
hafizrahman Jun 15, 2023
f6d69e3
Feat: fetch sharing suggestion from AI and display it.
hafizrahman Jun 15, 2023
3d395f0
Feat: handle share message editing. To be used later for actual sharing.
hafizrahman Jun 15, 2023
1d09516
Refactor: use fold instead for result handling.
hafizrahman Jun 15, 2023
76480e7
Refactor: Remove unneeded LoadingState and simplify code.
hafizrahman Jun 15, 2023
42b5ba1
Add: state update on result failure.
hafizrahman Jun 15, 2023
5e80da3
Refactor: make separate variables for button labels as they're to be …
hafizrahman Jun 15, 2023
1149c51
Add: result logging in AI Repository's fetching.
hafizrahman Jun 15, 2023
e13cbfd
Add: display error message on the UI during AI fetching error.
hafizrahman Jun 15, 2023
b07c7db
Update: Prompt for product sharing.
hafizrahman Jun 16, 2023
be9fb50
Feat: Open sharing intent when "Share" button in the AI sharing botto…
hafizrahman Jun 16, 2023
4765dad
Refactor: put sharing subject formatting in viewmodel instead.
hafizrahman Jun 16, 2023
a0171df
Feat: Make info button open Automattic's AI guidelines page.
hafizrahman Jun 16, 2023
5872af6
Add: Analytics events for AI sharing.
hafizrahman Jun 16, 2023
ce3f00e
Add: Analytics tracking for sheet displayed.
hafizrahman Jun 16, 2023
801311c
Add: Analytics tracking keys for AI sharing.
hafizrahman Jun 16, 2023
010290d
Add: Analytics tracking for button tapped.
hafizrahman Jun 16, 2023
fcab87a
Add: Analytics tracking for share button tapped (with some refactoring).
hafizrahman Jun 16, 2023
b5d5588
Add: Analytics tracking for share dialog dismissed.
hafizrahman Jun 16, 2023
92786e6
Add: Analytics tracking for AI message generated.
hafizrahman Jun 16, 2023
188d0ef
Add: Analytics tracking for AI message generation failure.
hafizrahman Jun 16, 2023
1a92fbf
Refactor: Make onGenerateButtonClicked() smaller function.
hafizrahman Jun 16, 2023
f60dfd5
Feat: enable Sharing product AI in feature flag.
hafizrahman Jun 16, 2023
e3a7be7
Add: release note.
hafizrahman Jun 16, 2023
f59f569
Fix: Detekt.
hafizrahman Jun 16, 2023
4c51592
Fix: use correct tracking for share.
hafizrahman Jun 16, 2023
61b0269
Refactor: Open product sharing feature only to WordPress.com-hosted s…
hafizrahman Jun 16, 2023
08d1c53
Use the isWPComAtomic property to correctly determine if site is wpcom
JorgeMucientes Jun 16, 2023
99eea26
Merge branch 'trunk' into feature/ai-share-description-functionality
JorgeMucientes Jun 16, 2023
501a406
Fix detekt indentation issue after merge with trunk
JorgeMucientes Jun 16, 2023
1a2c6e5
Fix: add checking for allowing a site to use sharing with AI.
hafizrahman Jun 16, 2023
c1903e4
Fix detekt spacing issue
JorgeMucientes Jun 16, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions RELEASE-NOTES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
- [*] [Internal] Added caching mechanism for JITMs [https://github.com/woocommerce/woocommerce-android/pull/9158]
- [***] Adds new entry point to promote product using Blaze [https://github.com/woocommerce/woocommerce-android/issues/9209]
- [*] [Internal] Fixed coupon validation logic [https://github.com/woocommerce/woocommerce-android/pull/9210]
- [***] AI: WordPress.com-hosted sites now has access to an experimental AI feature that generates product sharing messages for you. [https://github.com/woocommerce/woocommerce-android/pull/9236]

14.0
-----
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ object AppUrls {
const val AUTOMATTIC_PRIVACY_POLICY_CA = "https://automattic.com/privacy/#california-consumer-privacy-act-ccpa"
const val AUTOMATTIC_COOKIE_POLICY = "https://www.automattic.com/cookies"
const val AUTOMATTIC_HIRING = "https://automattic.com/work-with-us"
const val AUTOMATTIC_AI_GUIDELINES = "https://automattic.com/ai-guidelines/"

const val WOOCOMMERCE_UPGRADE = "https://docs.woocommerce.com/document/how-to-update-woocommerce/"
const val WOOCOMMERCE_PLUGIN = "https://wordpress.org/plugins/woocommerce/"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.woocommerce.android.ai

object AIPrompts {
private const val PRODUCT_DESCRIPTION_PROMPT = "Write a description for a product with title \"%1\$s\"%2\$s.\n" +
"Identify the language used in the product title and use the same language in your response.\n" +
"Make the description 50-60 words or less.\n" +
"Use a 9th grade reading level.\n" +
"Perform in-depth keyword research relating to the product in the same language of the product title, " +
"and use them in your sentences without listing them out."

fun generateProductDescriptionPrompt(name: String, features: String = ""): String {
val featuresPart = if (features.isNotEmpty()) " and features: \"$features\"" else ""
return String.format(PRODUCT_DESCRIPTION_PROMPT, name, featuresPart)
}
Comment on lines +4 to +14
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused yet, but will be very soon, so I thought to add it right away.


private const val PRODUCT_SHARING_PROMPT = "Your task is to help a merchant create a message to share with " +
"their customers a product named \"%1\$s\". More information about the product:\n" +
"%2\$s\n" +
"- Product URL: %3\$s.\n" +
"Identify the language used in the product name and product description, if any, to use in your response.\n" +
"The length should be up to 3 sentences.\n" +
"Use a 9th grade reading level.\n" +
"Add related hashtags at the end of the message.\n" +
"Do not include the URL in the message."

fun generateProductSharingPrompt(name: String, url: String, description: String = ""): String {
val descriptionPart = if (description.isNotEmpty()) "- Product description: \"$description\"" else ""
return String.format(PRODUCT_SHARING_PROMPT, name, descriptionPart, url)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.woocommerce.android.ai

import com.woocommerce.android.util.WooLog
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.wordpress.android.fluxc.model.SiteModel
import org.wordpress.android.fluxc.network.rest.wpcom.jetpackai.JetpackAIRestClient.JetpackAICompletionsResponse
import org.wordpress.android.fluxc.store.jetpackai.JetpackAIStore
import java.lang.Exception
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class AIRepository @Inject constructor(
private val jetpackAIStore: JetpackAIStore
) {
suspend fun fetchJetpackAICompletionsForSite(
site: SiteModel,
prompt: String,
skipCache: Boolean = false
): Result<String> = withContext(Dispatchers.IO) {
jetpackAIStore.fetchJetpackAICompletionsForSite(site, prompt, skipCache).run {
when (this) {
is JetpackAICompletionsResponse.Success -> {
WooLog.d(WooLog.T.AI, "Fetching Jetpack AI completions succeeded")
Result.success(completion)
}
is JetpackAICompletionsResponse.Error -> {
WooLog.w(WooLog.T.AI, "Fetching Jetpack AI completions failed: $message")
Result.failure(this.mapToException())
}
}
}
}
data class JetpackAICompletionsException(
val errorMessage: String,
val errorType: String
) : Exception(errorMessage)

private fun JetpackAICompletionsResponse.Error.mapToException() =
JetpackAICompletionsException(
errorMessage = message ?: "Unable to fetch AI completions",
errorType = type.name
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -857,6 +857,14 @@ enum class AnalyticsEvent(val siteless: Boolean = false) {
PRIVACY_CHOICES_BANNER_SETTINGS_BUTTON_TAPPED,
PRIVACY_CHOICES_BANNER_SAVE_BUTTON_TAPPED,

// AI Features
PRODUCT_SHARING_AI_DISPLAYED,
PRODUCT_SHARING_AI_GENERATE_TAPPED,
PRODUCT_SHARING_AI_SHARE_TAPPED,
PRODUCT_SHARING_AI_DISMISSED,
PRODUCT_SHARING_AI_MESSAGE_GENERATED,
PRODUCT_SHARING_AI_MESSAGE_GENERATION_FAILED,

// Blaze
BLAZE_ENTRY_POINT_DISPLAYED,
BLAZE_ENTRY_POINT_TAPPED,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,10 @@ class AnalyticsTracker private constructor(private val context: Context) {
const val VALUE_PRODUCT_SELECTOR = "product_selector"
const val VALUE_VARIATION_SELECTOR = "variation_selector"

// -- Product sharing with AI
const val KEY_IS_RETRY = "is_retry"
const val KEY_WITH_MESSAGE = "with_message"

// -- Blaze
const val KEY_BLAZE_SOURCE = "source"
const val KEY_BLAZE_STEP = "step"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import com.woocommerce.android.extensions.containsItem
import com.woocommerce.android.extensions.fastStripHtml
import com.woocommerce.android.extensions.getList
import com.woocommerce.android.extensions.isEmpty
import com.woocommerce.android.extensions.isSitePublic
import com.woocommerce.android.extensions.orNullIfEmpty
import com.woocommerce.android.extensions.removeItem
import com.woocommerce.android.media.MediaFilesRepository
Expand Down Expand Up @@ -396,7 +397,7 @@ class ProductDetailViewModel @Inject constructor(
)

viewState.productDraft?.let {
if (FeatureFlag.SHARING_PRODUCT_AI.isEnabled()) {
if (canSiteUseSharingWithAI()) {
triggerEvent(
ProductNavigationTarget.ShareProductWithAI(
it.permalink,
Expand All @@ -411,6 +412,12 @@ class ProductDetailViewModel @Inject constructor(
}
}

private fun canSiteUseSharingWithAI(): Boolean {
return FeatureFlag.SHARING_PRODUCT_AI.isEnabled() &&
selectedSite.get().isSitePublic &&
selectedSite.get().isWPComAtomic
}

fun onBlazeClicked() {
tracker.track(
stat = BLAZE_ENTRY_POINT_TAPPED,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ import com.woocommerce.android.viewmodel.MultiLiveEvent.Event
*/
sealed class ProductNavigationTarget : Event() {
data class ShareProduct(val url: String, val title: String) : ProductNavigationTarget()
data class ShareProductWithMessage(
val title: String,
val subject: String
) : ProductNavigationTarget()
data class ShareProductWithAI(
val permalink: String,
val title: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import com.woocommerce.android.ui.products.ProductNavigationTarget.NavigateToPro
import com.woocommerce.android.ui.products.ProductNavigationTarget.NavigateToVariationSelector
import com.woocommerce.android.ui.products.ProductNavigationTarget.RenameProductAttribute
import com.woocommerce.android.ui.products.ProductNavigationTarget.ShareProduct
import com.woocommerce.android.ui.products.ProductNavigationTarget.ShareProductWithAI
import com.woocommerce.android.ui.products.ProductNavigationTarget.ShareProductWithMessage
import com.woocommerce.android.ui.products.ProductNavigationTarget.ViewGroupedProducts
import com.woocommerce.android.ui.products.ProductNavigationTarget.ViewLinkedProducts
import com.woocommerce.android.ui.products.ProductNavigationTarget.ViewMediaUploadErrors
Expand Down Expand Up @@ -82,7 +84,18 @@ class ProductNavigator @Inject constructor() {
fragment.startActivity(Intent.createChooser(shareIntent, title))
}

is ProductNavigationTarget.ShareProductWithAI -> {
is ShareProductWithMessage -> {
val shareIntent: Intent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_TEXT, target.subject)
putExtra(Intent.EXTRA_TITLE, target.title)
type = "text/plain"
}
val title = fragment.resources.getText(R.string.product_share_dialog_title)
fragment.startActivity(Intent.createChooser(shareIntent, title))
}

is ShareProductWithAI -> {
val action = ProductDetailFragmentDirections
.actionProductDetailFragmentToProductSharingFragment(
target.permalink,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,28 +36,28 @@ import com.woocommerce.android.ui.products.ProductSharingViewModel.AIButtonState
import com.woocommerce.android.ui.products.ProductSharingViewModel.AIButtonState.Generating
import com.woocommerce.android.ui.products.ProductSharingViewModel.AIButtonState.Regenerate
import com.woocommerce.android.ui.products.ProductSharingViewModel.AIButtonState.WriteWithAI
import com.woocommerce.android.ui.products.ProductSharingViewModel.ViewState.ProductSharingViewState
import com.woocommerce.android.ui.products.ProductSharingViewModel.ProductSharingViewState

@Composable
fun ProductSharingBottomSheet(viewModel: ProductSharingViewModel) {
viewModel.viewState.observeAsState().value?.let {
when (it) {
is ProductSharingViewState -> {
ProductShareWithAI(
viewState = it,
onGenerateButtonClick = viewModel::onGenerateButtonClicked,
)
}

else -> { /* nothing to show for Loading state. */ }
}
ProductShareWithAI(
viewState = it,
onGenerateButtonClick = viewModel::onGenerateButtonClicked,
onShareMessageEdit = viewModel::onShareMessageEdited,
onSharingButtonClick = viewModel::onShareButtonClicked,
onInfoButtonClick = viewModel::onInfoButtonClicked
)
}
}

@Composable
fun ProductShareWithAI(
viewState: ProductSharingViewState,
onGenerateButtonClick: () -> Unit = {}
onGenerateButtonClick: () -> Unit = {},
onShareMessageEdit: (String) -> Unit = {},
onSharingButtonClick: () -> Unit = {},
onInfoButtonClick: () -> Unit = {}
) {
Column(
modifier = Modifier
Expand All @@ -80,11 +80,15 @@ fun ProductShareWithAI(
if (viewState.isGenerating) {
SharingMessageSkeletonView()
} else {
val isError = viewState.errorMessage.isNotEmpty()

WCOutlinedTextField(
value = viewState.shareMessage,
onValueChange = { /*TODO*/ },
onValueChange = { onShareMessageEdit(it) },
label = stringResource(id = R.string.product_sharing_optional_message_label),
maxLines = 4
maxLines = 5,
isError = isError,
helperText = if (isError) viewState.errorMessage else null
)
}

Expand All @@ -102,7 +106,7 @@ fun ProductShareWithAI(
}

IconButton(
onClick = { /* TODO */ },
onClick = onInfoButtonClick,
enabled = !viewState.isGenerating
) {
Icon(
Expand All @@ -116,7 +120,7 @@ fun ProductShareWithAI(
}

WCColoredButton(
onClick = { /*TODO*/ },
onClick = onSharingButtonClick,
modifier = Modifier.fillMaxWidth(),
enabled = !viewState.isGenerating
) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.woocommerce.android.ui.products

import android.content.DialogInterface
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
Expand All @@ -9,10 +10,15 @@ import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.fragment.app.viewModels
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.woocommerce.android.ui.compose.theme.WooThemeWithBackground
import com.woocommerce.android.util.ChromeCustomTabUtils
import com.woocommerce.android.viewmodel.MultiLiveEvent.Event.LaunchUrlInChromeTab
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject

@AndroidEntryPoint
class ProductSharingDialog : BottomSheetDialogFragment() {
@Inject
lateinit var navigator: ProductNavigator
private val viewModel: ProductSharingViewModel by viewModels()

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
Expand All @@ -26,4 +32,18 @@ class ProductSharingDialog : BottomSheetDialogFragment() {
}
}
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
viewModel.event.observe(viewLifecycleOwner) { event ->
when (event) {
is ProductNavigationTarget -> navigator.navigate(this, event)
is LaunchUrlInChromeTab -> ChromeCustomTabUtils.launchUrl(requireContext(), event.url)
}
}
}

override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
viewModel.onDialogDismissed()
}
}
Loading