diff --git a/packages/shared/src/components/buttons/common.ts b/packages/shared/src/components/buttons/common.ts index d1214e1bff..a0b4e4ded3 100644 --- a/packages/shared/src/components/buttons/common.ts +++ b/packages/shared/src/components/buttons/common.ts @@ -209,9 +209,18 @@ export const useGetIconWithSize = ( size: icon.props?.size ?? buttonSizeToIconSize[size], className: classNames( icon.props.className, - !iconOnly && '!h-6 !w-6 text-base', - !iconOnly && iconPosition === ButtonIconPosition.Left && '-ml-2 mr-1', - !iconOnly && iconPosition === ButtonIconPosition.Right && '-mr-2 ml-1', + !iconOnly && 'text-base', + !iconOnly && !icon.props?.size && '!h-6 !w-6', + !iconOnly && iconPosition === ButtonIconPosition.Left && 'mr-1', + !iconOnly && + !icon.props?.size && + iconPosition === ButtonIconPosition.Left && + '-ml-2', + !iconOnly && iconPosition === ButtonIconPosition.Right && 'ml-1', + !iconOnly && + !icon.props?.size && + iconPosition === ButtonIconPosition.Right && + '-mr-2', ), }); }; diff --git a/packages/shared/src/components/cards/common/ShowMoreContent.tsx b/packages/shared/src/components/cards/common/ShowMoreContent.tsx index d10a2c46b9..8c46380eb9 100644 --- a/packages/shared/src/components/cards/common/ShowMoreContent.tsx +++ b/packages/shared/src/components/cards/common/ShowMoreContent.tsx @@ -38,7 +38,10 @@ export default function ShowMoreContent({ return (
-

+

{contentPrefix} {getContent()}{' '} {displayShowMoreLink() && ( diff --git a/packages/shared/src/components/feeds/FeedSettings/components/SmartPrompts.tsx b/packages/shared/src/components/feeds/FeedSettings/components/SmartPrompts.tsx new file mode 100644 index 0000000000..e909a194cd --- /dev/null +++ b/packages/shared/src/components/feeds/FeedSettings/components/SmartPrompts.tsx @@ -0,0 +1,126 @@ +import React from 'react'; +import type { ReactElement } from 'react'; +import { + Typography, + TypographyColor, + TypographyTag, + TypographyType, +} from '../../../typography/Typography'; +import { PlusUser } from '../../../PlusUser'; +import { LogEvent, Origin, TargetId } from '../../../../lib/log'; +import { Button, ButtonSize, ButtonVariant } from '../../../buttons/Button'; +import { plusUrl } from '../../../../lib/constants'; +import { DevPlusIcon } from '../../../icons'; +import { usePlusSubscription, useToastNotification } from '../../../../hooks'; +import { usePromptsQuery } from '../../../../hooks/prompt/usePromptsQuery'; +import { FilterCheckbox } from '../../../fields/FilterCheckbox'; +import { useSettingsContext } from '../../../../contexts/SettingsContext'; +import { labels } from '../../../../lib'; +import { useLogContext } from '../../../../contexts/LogContext'; +import { useFeedSettingsEditContext } from '../FeedSettingsEditContext'; +import { SimpleTooltip } from '../../../tooltips'; +import ConditionalWrapper from '../../../ConditionalWrapper'; + +export const SmartPrompts = (): ReactElement => { + const { editFeedSettings } = useFeedSettingsEditContext(); + const { isPlus, logSubscriptionEvent } = usePlusSubscription(); + const { displayToast } = useToastNotification(); + const { logEvent } = useLogContext(); + const { flags, updatePromptFlag } = useSettingsContext(); + const { prompt: promptFlags } = flags; + const { data: prompts, isLoading } = usePromptsQuery(); + + return ( +

+
+
+ + Smart Prompts + + +
+ + Level up how you interact with posts using AI-powered prompts. Extract + insights, refine content, or run custom instructions to get more out + of every post in one click. + +
+ +
+ {prompts?.map(({ id, label, description }) => ( + { + return ( + +
{child as ReactElement}
+
+ ); + }} + > + { + const newState = !promptFlags?.[id] || false; + editFeedSettings(() => updatePromptFlag(id, newState)); + displayToast( + labels.feed.settings.globalPreferenceNotice.smartPrompt, + ); + + logEvent({ + event_name: LogEvent.ToggleSmartPrompts, + target_type: id, + target_id: newState ? TargetId.On : TargetId.Off, + extra: JSON.stringify({ + origin: Origin.Settings, + }), + }); + }} + descriptionClassName="text-text-tertiary" + > + {label} + +
+ ))} +
+ {!isPlus && ( + + )} +
+ ); +}; diff --git a/packages/shared/src/components/feeds/FeedSettings/sections/FeedSettingsAISection.tsx b/packages/shared/src/components/feeds/FeedSettings/sections/FeedSettingsAISection.tsx index 02972ae760..2bf7a27297 100644 --- a/packages/shared/src/components/feeds/FeedSettings/sections/FeedSettingsAISection.tsx +++ b/packages/shared/src/components/feeds/FeedSettings/sections/FeedSettingsAISection.tsx @@ -25,6 +25,7 @@ import { import { Divider } from '../../../utilities'; import { Switch } from '../../../fields/Switch'; import { labels } from '../../../../lib'; +import { SmartPrompts } from '../components/SmartPrompts'; export const FeedSettingsAISection = (): ReactElement => { const { isPlus, logSubscriptionEvent } = usePlusSubscription(); @@ -38,6 +39,39 @@ export const FeedSettingsAISection = (): ReactElement => { return ( <> +
+
+ + Preferred language + + + Choose your preferred language for the post titles on the feed + +
+ { + onLanguageChange(value); + + displayToast( + labels.feed.settings.globalPreferenceNotice.contentLanguage, + ); + }} + icon={null} + /> +
+
@@ -127,38 +161,7 @@ export const FeedSettingsAISection = (): ReactElement => { )}
-
-
- - Preferred language - - - Choose your preferred language for the post titles on the feed - -
- { - onLanguageChange(value); - - displayToast( - labels.feed.settings.globalPreferenceNotice.contentLanguage, - ); - }} - icon={null} - /> -
+ ); }; diff --git a/packages/shared/src/components/icons/CustomPrompt/filled.svg b/packages/shared/src/components/icons/CustomPrompt/filled.svg new file mode 100644 index 0000000000..5f0275b0a4 --- /dev/null +++ b/packages/shared/src/components/icons/CustomPrompt/filled.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/packages/shared/src/components/icons/CustomPrompt/index.tsx b/packages/shared/src/components/icons/CustomPrompt/index.tsx new file mode 100644 index 0000000000..12013d0dec --- /dev/null +++ b/packages/shared/src/components/icons/CustomPrompt/index.tsx @@ -0,0 +1,10 @@ +import type { ReactElement } from 'react'; +import React from 'react'; +import type { IconProps } from '../../Icon'; +import Icon from '../../Icon'; +import OutlinedIcon from './outlined.svg'; +import FilledIcon from './filled.svg'; + +export const CustomPromptIcon = (props: IconProps): ReactElement => ( + +); diff --git a/packages/shared/src/components/icons/CustomPrompt/outlined.svg b/packages/shared/src/components/icons/CustomPrompt/outlined.svg new file mode 100644 index 0000000000..88dbaaa14f --- /dev/null +++ b/packages/shared/src/components/icons/CustomPrompt/outlined.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/packages/shared/src/components/icons/EditPrompt/filled.svg b/packages/shared/src/components/icons/EditPrompt/filled.svg new file mode 100644 index 0000000000..50831dfe79 --- /dev/null +++ b/packages/shared/src/components/icons/EditPrompt/filled.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/shared/src/components/icons/EditPrompt/index.tsx b/packages/shared/src/components/icons/EditPrompt/index.tsx new file mode 100644 index 0000000000..9fe1983544 --- /dev/null +++ b/packages/shared/src/components/icons/EditPrompt/index.tsx @@ -0,0 +1,10 @@ +import type { ReactElement } from 'react'; +import React from 'react'; +import type { IconProps } from '../../Icon'; +import Icon from '../../Icon'; +import OutlinedIcon from './outlined.svg'; +import FilledIcon from './filled.svg'; + +export const EditPromptIcon = (props: IconProps): ReactElement => ( + +); diff --git a/packages/shared/src/components/icons/EditPrompt/outlined.svg b/packages/shared/src/components/icons/EditPrompt/outlined.svg new file mode 100644 index 0000000000..0005ac16c4 --- /dev/null +++ b/packages/shared/src/components/icons/EditPrompt/outlined.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/shared/src/components/icons/TLDR/filled.svg b/packages/shared/src/components/icons/TLDR/filled.svg new file mode 100644 index 0000000000..ee8f08331d --- /dev/null +++ b/packages/shared/src/components/icons/TLDR/filled.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/shared/src/components/icons/TLDR/index.tsx b/packages/shared/src/components/icons/TLDR/index.tsx new file mode 100644 index 0000000000..16e65ab51b --- /dev/null +++ b/packages/shared/src/components/icons/TLDR/index.tsx @@ -0,0 +1,10 @@ +import type { ReactElement } from 'react'; +import React from 'react'; +import type { IconProps } from '../../Icon'; +import Icon from '../../Icon'; +import OutlinedIcon from './outlined.svg'; +import FilledIcon from './filled.svg'; + +export const TLDRIcon = (props: IconProps): ReactElement => ( + +); diff --git a/packages/shared/src/components/icons/TLDR/outlined.svg b/packages/shared/src/components/icons/TLDR/outlined.svg new file mode 100644 index 0000000000..075d96a96f --- /dev/null +++ b/packages/shared/src/components/icons/TLDR/outlined.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/shared/src/components/icons/index.ts b/packages/shared/src/components/icons/index.ts index b9a8517e7b..bc1d0b8d69 100644 --- a/packages/shared/src/components/icons/index.ts +++ b/packages/shared/src/components/icons/index.ts @@ -129,4 +129,7 @@ export * from './ShieldWarning'; export * from './ShieldPlus'; export * from './Sidebar'; export * from './Folder'; +export * from './EditPrompt'; +export * from './CustomPrompt'; +export * from './TLDR'; export * from './Privacy'; diff --git a/packages/shared/src/components/modals/common.tsx b/packages/shared/src/components/modals/common.tsx index 45ccf4f0ba..bdb040887a 100644 --- a/packages/shared/src/components/modals/common.tsx +++ b/packages/shared/src/components/modals/common.tsx @@ -214,6 +214,12 @@ const AddToCustomFeedModal = dynamic( ), ); +const SmartPromptModal = dynamic(() => + import( + /* webpackChunkName: "smartPromptModal" */ './plus/SmartPromptModal' + ).then((mod) => mod.SmartPromptModal), +); + const CookieConsentModal = dynamic( () => import( @@ -264,6 +270,7 @@ export const modals = { [LazyModal.ClickbaitShield]: ClickbaitShieldModal, [LazyModal.MoveBookmark]: MoveBookmarkModal, [LazyModal.AddToCustomFeed]: AddToCustomFeedModal, + [LazyModal.SmartPrompt]: SmartPromptModal, [LazyModal.CookieConsent]: CookieConsentModal, [LazyModal.ReportUser]: ReportUserModal, }; diff --git a/packages/shared/src/components/modals/common/types.ts b/packages/shared/src/components/modals/common/types.ts index 50a9914e82..2dcc154984 100644 --- a/packages/shared/src/components/modals/common/types.ts +++ b/packages/shared/src/components/modals/common/types.ts @@ -62,6 +62,7 @@ export enum LazyModal { AddToCustomFeed = 'addToCustomFeed', CookieConsent = 'cookieConsent', ReportUser = 'reportUser', + SmartPrompt = 'smartPrompt', } export type ModalTabItem = { diff --git a/packages/shared/src/components/modals/plus/SmartPromptModal.tsx b/packages/shared/src/components/modals/plus/SmartPromptModal.tsx new file mode 100644 index 0000000000..4b00d30428 --- /dev/null +++ b/packages/shared/src/components/modals/plus/SmartPromptModal.tsx @@ -0,0 +1,62 @@ +import type { ReactElement } from 'react'; +import React from 'react'; +import type { ModalProps } from '../common/Modal'; +import { Modal } from '../common/Modal'; +import { Image } from '../../image/Image'; +import { smartPromptModalImage } from '../../../lib/image'; +import { + Typography, + TypographyColor, + TypographyType, +} from '../../typography/Typography'; +import { PlusUser } from '../../PlusUser'; +import { Button, ButtonVariant } from '../../buttons/Button'; +import { webappUrl } from '../../../lib/constants'; +import { DevPlusIcon } from '../../icons'; +import { LogEvent, TargetId } from '../../../lib/log'; +import { usePlusSubscription } from '../../../hooks'; + +export const SmartPromptModal = ({ ...props }: ModalProps): ReactElement => { + const { logSubscriptionEvent } = usePlusSubscription(); + return ( + + Smart Prompt feature +
+
+ + Smart Prompts + + +
+ + + Level up how you interact with posts using AI-powered prompts. Extract + insights, refine content, or run custom instructions to get more out + of every post in one click. + + +
+
+ ); +}; diff --git a/packages/shared/src/components/plus/ClickbaitTrial.tsx b/packages/shared/src/components/plus/PostUpgradeToPlus.tsx similarity index 62% rename from packages/shared/src/components/plus/ClickbaitTrial.tsx rename to packages/shared/src/components/plus/PostUpgradeToPlus.tsx index 30ac12626e..cc37054f6b 100644 --- a/packages/shared/src/components/plus/ClickbaitTrial.tsx +++ b/packages/shared/src/components/plus/PostUpgradeToPlus.tsx @@ -1,5 +1,6 @@ -import type { ReactElement } from 'react'; -import React, { useState } from 'react'; +import type { PropsWithChildren, ReactElement } from 'react'; +import React, { useCallback, useState } from 'react'; +import classNames from 'classnames'; import { PlusUser } from '../PlusUser'; import CloseButton from '../CloseButton'; import { ButtonSize, ButtonVariant } from '../buttons/common'; @@ -12,26 +13,48 @@ import { Button } from '../buttons/Button'; import { webappUrl } from '../../lib/constants'; import { DevPlusIcon } from '../icons'; import { usePlusSubscription } from '../../hooks'; -import { LogEvent, TargetId } from '../../lib/log'; +import type { TargetId } from '../../lib/log'; +import { LogEvent } from '../../lib/log'; -export const ClickbaitTrial = (): ReactElement => { +type PostUpgradeToPlusProps = { + targetId: TargetId; + title: ReactElement | string; + className?: string; + onClose?: () => void; +}; + +export const PostUpgradeToPlus = ({ + targetId: target_id, + title, + children, + className, + onClose, +}: PostUpgradeToPlusProps & PropsWithChildren): ReactElement => { const [show, setShow] = useState(true); const { logSubscriptionEvent } = usePlusSubscription(); + const onCloseClick = useCallback(() => { + onClose?.(); + setShow(false); + }, [onClose]); + if (!show) { return null; } return ( -
+
{ - setShow(false); - }} + onClick={onCloseClick} />
{ color={TypographyColor.Primary} className="py-2" > - Want to automatically optimize titles across your feed? - - - Clickbait Shield uses AI to automatically optimize post titles by fixing - common problems like clickbait, lack of clarity, and overly promotional - language. -
-
- The result is clearer, more informative titles that help you quickly - find the content you actually need. + {title}
+ {children}
diff --git a/packages/shared/src/components/post/PostContent.tsx b/packages/shared/src/components/post/PostContent.tsx index fab1cc4491..7e84e243b7 100644 --- a/packages/shared/src/components/post/PostContent.tsx +++ b/packages/shared/src/components/post/PostContent.tsx @@ -4,7 +4,6 @@ import React, { useEffect } from 'react'; import dynamic from 'next/dynamic'; import { isVideoPost } from '../../graphql/posts'; import PostMetadata from '../cards/common/PostMetadata'; -import PostSummary from '../cards/common/PostSummary'; import { PostWidgets } from './PostWidgets'; import { TagLinks } from '../TagLinks'; import PostToc from '../widgets/PostToc'; @@ -27,6 +26,7 @@ import { withPostById } from './withPostById'; import { PostClickbaitShield } from './common/PostClickbaitShield'; import { useSmartTitle } from '../../hooks/post/useSmartTitle'; import { SharedByUserBanner } from '../SharedByUserBanner'; +import { SmartPrompt } from './smartPrompts/SmartPrompt'; export const SCROLL_OFFSET = 80; export const ONBOARDING_OFFSET = 120; @@ -164,9 +164,7 @@ export function PostContentRaw({ className="mb-7" /> )} - {post.summary && ( - - )} + {post.summary && } { const { openModal } = useLazyModal(); @@ -57,7 +58,19 @@ export const PostClickbaitShield = ({ post }: { post: Post }): ReactElement => { {fetchedSmartTitle ? ( <> This title was optimized with Clickbait Shield - + + Clickbait Shield uses AI to automatically optimize post titles by + fixing common problems like clickbait, lack of clarity, and overly + promotional language. +
+
+ The result is clearer, more informative titles that help you + quickly find the content you actually need. +
) : ( <> diff --git a/packages/shared/src/components/post/smartPrompts/CustomPrompt.tsx b/packages/shared/src/components/post/smartPrompts/CustomPrompt.tsx new file mode 100644 index 0000000000..0db8fa92a9 --- /dev/null +++ b/packages/shared/src/components/post/smartPrompts/CustomPrompt.tsx @@ -0,0 +1,118 @@ +import React, { useCallback, useMemo, useState } from 'react'; +import type { ReactElement } from 'react'; +import { + Button, + ButtonColor, + ButtonSize, + ButtonVariant, +} from '../../buttons/Button'; +import type { Post } from '../../../graphql/posts'; +import { useSmartPrompt } from '../../../hooks/prompt/useSmartPrompt'; +import { usePromptsQuery } from '../../../hooks/prompt/usePromptsQuery'; +import { PromptDisplay } from '../../../graphql/prompt'; +import { SearchProgressBar } from '../../search'; +import { isNullOrUndefined } from '../../../lib/func'; +import Alert, { AlertType } from '../../widgets/Alert'; +import { labels } from '../../../lib'; +import { RenderMarkdown } from '../../RenderMarkdown'; +import { CopyIcon, EditIcon } from '../../icons'; +import { useCopyText } from '../../../hooks/useCopy'; +import { postLogEvent } from '../../../lib/feed'; +import { LogEvent } from '../../../lib/log'; +import { useLogContext } from '../../../contexts/LogContext'; + +type CustomPromptProps = { + post: Post; +}; + +export const CustomPrompt = ({ post }: CustomPromptProps): ReactElement => { + const { logEvent } = useLogContext(); + const { data: prompts } = usePromptsQuery(); + const [isEdit, setIsEdit] = useState(false); + const [copying, copy] = useCopyText(); + const prompt = useMemo( + () => prompts?.find((p) => p.id === PromptDisplay.CustomPrompt), + [prompts], + ); + const { executePrompt, data, isPending } = useSmartPrompt({ post, prompt }); + const onSubmitCustomPrompt = useCallback( + (e) => { + e.preventDefault(); + logEvent( + postLogEvent(LogEvent.SmartPrompt, post, { + extra: { prompt: 'custom-prompt' }, + }), + ); + setIsEdit(false); + executePrompt(e.target[0].value); + }, + [executePrompt, logEvent, post], + ); + + if (!data || isEdit) { + return ( +
+