From d38297aa1f6caca10b5bffb24e173a41af654760 Mon Sep 17 00:00:00 2001 From: Ole-Martin Bratteng <1681525+omBratteng@users.noreply.github.com> Date: Thu, 2 Jan 2025 16:02:40 +0100 Subject: [PATCH 01/14] feat: add `EditPrompt` icon --- .../shared/src/components/icons/EditPrompt/filled.svg | 5 +++++ .../shared/src/components/icons/EditPrompt/index.tsx | 10 ++++++++++ .../src/components/icons/EditPrompt/outlined.svg | 4 ++++ packages/shared/src/components/icons/index.ts | 1 + 4 files changed, 20 insertions(+) create mode 100644 packages/shared/src/components/icons/EditPrompt/filled.svg create mode 100644 packages/shared/src/components/icons/EditPrompt/index.tsx create mode 100644 packages/shared/src/components/icons/EditPrompt/outlined.svg 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/index.ts b/packages/shared/src/components/icons/index.ts index 8a23faebd7..dd2beb3a00 100644 --- a/packages/shared/src/components/icons/index.ts +++ b/packages/shared/src/components/icons/index.ts @@ -127,3 +127,4 @@ export * from './ShieldWarning'; export * from './ShieldPlus'; export * from './Sidebar'; export * from './Folder'; +export * from './EditPrompt'; From 26e23b54fcc65d93efdd983a9cb0ab231732a152 Mon Sep 17 00:00:00 2001 From: Ole-Martin Bratteng <1681525+omBratteng@users.noreply.github.com> Date: Thu, 2 Jan 2025 16:02:49 +0100 Subject: [PATCH 02/14] feat: add `CustomPrompt` icon --- .../src/components/icons/CustomPrompt/filled.svg | 10 ++++++++++ .../shared/src/components/icons/CustomPrompt/index.tsx | 10 ++++++++++ .../src/components/icons/CustomPrompt/outlined.svg | 10 ++++++++++ packages/shared/src/components/icons/index.ts | 1 + 4 files changed, 31 insertions(+) create mode 100644 packages/shared/src/components/icons/CustomPrompt/filled.svg create mode 100644 packages/shared/src/components/icons/CustomPrompt/index.tsx create mode 100644 packages/shared/src/components/icons/CustomPrompt/outlined.svg 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/index.ts b/packages/shared/src/components/icons/index.ts index dd2beb3a00..2039c917bd 100644 --- a/packages/shared/src/components/icons/index.ts +++ b/packages/shared/src/components/icons/index.ts @@ -128,3 +128,4 @@ export * from './ShieldPlus'; export * from './Sidebar'; export * from './Folder'; export * from './EditPrompt'; +export * from './CustomPrompt'; From a511e2c6d9a1fb9a395ed8df4396d4864a149789 Mon Sep 17 00:00:00 2001 From: Ole-Martin Bratteng <1681525+omBratteng@users.noreply.github.com> Date: Thu, 2 Jan 2025 16:02:56 +0100 Subject: [PATCH 03/14] feat: add `TLDR` icon --- packages/shared/src/components/icons/TLDR/filled.svg | 6 ++++++ packages/shared/src/components/icons/TLDR/index.tsx | 10 ++++++++++ packages/shared/src/components/icons/TLDR/outlined.svg | 6 ++++++ packages/shared/src/components/icons/index.ts | 1 + 4 files changed, 23 insertions(+) create mode 100644 packages/shared/src/components/icons/TLDR/filled.svg create mode 100644 packages/shared/src/components/icons/TLDR/index.tsx create mode 100644 packages/shared/src/components/icons/TLDR/outlined.svg 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 2039c917bd..7bb15fe47a 100644 --- a/packages/shared/src/components/icons/index.ts +++ b/packages/shared/src/components/icons/index.ts @@ -129,3 +129,4 @@ export * from './Sidebar'; export * from './Folder'; export * from './EditPrompt'; export * from './CustomPrompt'; +export * from './TLDR'; From 0a3f255f31577a41165c4c7f6a80de843ae17e2b Mon Sep 17 00:00:00 2001 From: Chris Bongers Date: Fri, 3 Jan 2025 11:59:06 +0200 Subject: [PATCH 04/14] feat: setting addition (#4035) --- .../FeedSettings/components/SmartPrompts.tsx | 121 ++++++++++++++++++ .../sections/FeedSettingsAISection.tsx | 3 + .../shared/src/contexts/SettingsContext.tsx | 12 ++ packages/shared/src/graphql/prompt.ts | 29 +++++ packages/shared/src/graphql/settings.ts | 1 + .../src/hooks/prompt/usePromptsQuery.ts | 40 ++++++ packages/shared/src/lib/labels.ts | 1 + packages/shared/src/lib/log.ts | 3 + packages/shared/src/lib/query.ts | 1 + 9 files changed, 211 insertions(+) create mode 100644 packages/shared/src/components/feeds/FeedSettings/components/SmartPrompts.tsx create mode 100644 packages/shared/src/graphql/prompt.ts create mode 100644 packages/shared/src/hooks/prompt/usePromptsQuery.ts 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..c85739d552 --- /dev/null +++ b/packages/shared/src/components/feeds/FeedSettings/components/SmartPrompts.tsx @@ -0,0 +1,121 @@ +import React from 'react'; +import type { ReactElement } from 'react'; +import { + Typography, + TypographyColor, + TypographyTag, + TypographyType, +} from '../../../typography/Typography'; +import { PlusUser } from '../../../PlusUser'; +import ConditionalWrapper from '../../../ConditionalWrapper'; +import { SimpleTooltip } from '../../../tooltips'; +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'; + +export const SmartPrompts = (): ReactElement => { + 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. + +
+ { + return ( + +
{child as ReactElement}
+
+ ); + }} + > +
+ {prompts?.map(({ id, label, description }) => ( + { + const newState = !(promptFlags?.[id] || true); + updatePromptFlag(id, newState); + displayToast( + labels.feed.settings.globalPreferenceNotice.smartPrompt, + ); + + logEvent({ + event_name: LogEvent.ToggleSmartPrompts, + 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 e325c2bcd0..49466f340b 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, showPlusSubscription, logSubscriptionEvent } = @@ -131,6 +132,8 @@ export const FeedSettingsAISection = (): ReactElement => { )} + + )}
diff --git a/packages/shared/src/contexts/SettingsContext.tsx b/packages/shared/src/contexts/SettingsContext.tsx index dea724033b..be50e31b22 100644 --- a/packages/shared/src/contexts/SettingsContext.tsx +++ b/packages/shared/src/contexts/SettingsContext.tsx @@ -56,6 +56,7 @@ export interface SettingsContextData extends Omit { loadedSettings: boolean; updateCustomLinks: (links: string[]) => Promise; updateFlag: (flag: keyof SettingsFlags, value: boolean) => Promise; + updatePromptFlag: (flag: string, value: boolean) => Promise; syncSettings: (bootUserId?: string) => Promise; onToggleHeaderPlacement(): Promise; setOnboardingChecklistView: (value: ChecklistViewState) => Promise; @@ -275,6 +276,17 @@ export const SettingsContextProvider = ({ [flag]: value, }, }), + updatePromptFlag: (flag: keyof SettingsFlags, value: boolean) => + setSettings({ + ...settings, + flags: { + ...settings.flags, + prompt: { + ...settings.flags.prompt, + [flag]: value, + }, + }, + }), setSettings, applyThemeMode, }), diff --git a/packages/shared/src/graphql/prompt.ts b/packages/shared/src/graphql/prompt.ts new file mode 100644 index 0000000000..7afa890bdb --- /dev/null +++ b/packages/shared/src/graphql/prompt.ts @@ -0,0 +1,29 @@ +import { gql } from 'graphql-request'; + +export type PromptFlags = { + icon?: string; + color?: string; +}; + +export type Prompt = { + id: string; + label: string; + description?: string; + createdAt: Date; + updatedAt: Date; + flags?: PromptFlags; +}; + +export const PROMPTS_QUERY = gql` + query Prompts { + prompts { + id + label + description + flags { + icon + color + } + } + } +`; diff --git a/packages/shared/src/graphql/settings.ts b/packages/shared/src/graphql/settings.ts index 43caaa3408..7bd52f309c 100644 --- a/packages/shared/src/graphql/settings.ts +++ b/packages/shared/src/graphql/settings.ts @@ -16,6 +16,7 @@ export type SettingsFlags = { sidebarResourcesExpanded: boolean; sidebarBookmarksExpanded: boolean; clickbaitShieldEnabled: boolean; + prompt?: Record; }; export enum SidebarSettingsFlags { diff --git a/packages/shared/src/hooks/prompt/usePromptsQuery.ts b/packages/shared/src/hooks/prompt/usePromptsQuery.ts new file mode 100644 index 0000000000..1e775e955a --- /dev/null +++ b/packages/shared/src/hooks/prompt/usePromptsQuery.ts @@ -0,0 +1,40 @@ +import type { UseQueryOptions, UseQueryResult } from '@tanstack/react-query'; +import { useQuery } from '@tanstack/react-query'; +import { generateQueryKey, RequestKey, StaleTime } from '../../lib/query'; +import { gqlClient } from '../../graphql/common'; +import { useAuthContext } from '../../contexts/AuthContext'; +import type { Prompt } from '../../graphql/prompt'; +import { PROMPTS_QUERY } from '../../graphql/prompt'; + +type UsePromptsQueryProps = { + queryOptions?: Partial>; +}; + +type UsePromptsQuery = UseQueryResult; + +export const usePromptsQuery = ({ + queryOptions, +}: UsePromptsQueryProps = {}): UsePromptsQuery => { + const { user } = useAuthContext(); + const enabled = !!user; + + const queryResult = useQuery({ + queryKey: generateQueryKey(RequestKey.Prompts, user), + + queryFn: async () => { + const result = await gqlClient.request<{ + prompts: Prompt[]; + }>(PROMPTS_QUERY); + + return result.prompts; + }, + staleTime: StaleTime.OneHour, + ...queryOptions, + enabled: + typeof queryOptions?.enabled !== 'undefined' + ? queryOptions.enabled && enabled + : enabled, + }); + + return queryResult; +}; diff --git a/packages/shared/src/lib/labels.ts b/packages/shared/src/lib/labels.ts index cfd9b9343e..c46aff67d7 100644 --- a/packages/shared/src/lib/labels.ts +++ b/packages/shared/src/lib/labels.ts @@ -63,6 +63,7 @@ export const labels = { globalPreferenceNotice: { clickbaitShield: 'Clickbait shield has been applied for all feeds', contentLanguage: 'New language preferences set for all feeds', + smartPrompt: 'Smart Prompt setting has been applied for all feeds', }, }, }, diff --git a/packages/shared/src/lib/log.ts b/packages/shared/src/lib/log.ts index a8d66a083c..0b7f88bd8c 100644 --- a/packages/shared/src/lib/log.ts +++ b/packages/shared/src/lib/log.ts @@ -235,6 +235,9 @@ export enum LogEvent { ShareSource = 'share source', ShareTag = 'share tag', // End Share + // Start Smart Prompts + ToggleSmartPrompts = 'toggle smart prompts', + // End Smart Prompts } export enum FeedItemTitle { diff --git a/packages/shared/src/lib/query.ts b/packages/shared/src/lib/query.ts index 4945f159f2..8c4d844c69 100644 --- a/packages/shared/src/lib/query.ts +++ b/packages/shared/src/lib/query.ts @@ -186,6 +186,7 @@ export enum RequestKey { UserShortById = 'user_short_by_id', BookmarkFolders = 'bookmark_folders', FetchedOriginalTitle = 'fetched_original_title', + Prompts = 'prompts', } export type HasConnection< From 1667a4aa3554909ee54378d8ce0f60b434085c72 Mon Sep 17 00:00:00 2001 From: Chris Bongers Date: Fri, 3 Jan 2025 14:50:36 +0200 Subject: [PATCH 05/14] feat: smart prompt modal (#4038) --- .../shared/src/components/modals/common.tsx | 7 +++ .../src/components/modals/common/types.ts | 1 + .../modals/plus/SmartPromptModal.tsx | 63 +++++++++++++++++++ packages/shared/src/lib/log.ts | 1 + 4 files changed, 72 insertions(+) create mode 100644 packages/shared/src/components/modals/plus/SmartPromptModal.tsx diff --git a/packages/shared/src/components/modals/common.tsx b/packages/shared/src/components/modals/common.tsx index f0f6af6364..62700a3042 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), +); + export const modals = { [LazyModal.SquadMember]: SquadMemberModal, [LazyModal.UpvotedPopup]: UpvotedPopupModal, @@ -250,6 +256,7 @@ export const modals = { [LazyModal.ClickbaitShield]: ClickbaitShieldModal, [LazyModal.MoveBookmark]: MoveBookmarkModal, [LazyModal.AddToCustomFeed]: AddToCustomFeedModal, + [LazyModal.SmartPrompt]: SmartPromptModal, }; type GetComponentProps = T extends diff --git a/packages/shared/src/components/modals/common/types.ts b/packages/shared/src/components/modals/common/types.ts index 00832f1e0b..25c8f6cc5b 100644 --- a/packages/shared/src/components/modals/common/types.ts +++ b/packages/shared/src/components/modals/common/types.ts @@ -60,6 +60,7 @@ export enum LazyModal { ClickbaitShield = 'clickbaitShield', MoveBookmark = 'moveBookmark', AddToCustomFeed = 'addToCustomFeed', + 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..d343077e6a --- /dev/null +++ b/packages/shared/src/components/modals/plus/SmartPromptModal.tsx @@ -0,0 +1,63 @@ +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 { clickbaitShieldModalImage } 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(); + // TODO: Add correct image below + 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/lib/log.ts b/packages/shared/src/lib/log.ts index 0b7f88bd8c..e02360e55a 100644 --- a/packages/shared/src/lib/log.ts +++ b/packages/shared/src/lib/log.ts @@ -315,6 +315,7 @@ export enum TargetId { BookmarkFolder = 'bookmark folder', FeedSettings = 'feed settings', ClickbaitShield = 'clickbait shield', + SmartPrompt = 'smart prompt', } export enum NotificationChannel { From 42d0758cffc07b27b2cc21640091135f6f39a759 Mon Sep 17 00:00:00 2001 From: Ole-Martin Bratteng <1681525+omBratteng@users.noreply.github.com> Date: Mon, 6 Jan 2025 14:00:41 +0100 Subject: [PATCH 06/14] feat: add custom prompt buttons to post content (#4039) Co-authored-by: Chris Bongers --- .../shared/src/components/buttons/common.ts | 15 +- ...ickbaitTrial.tsx => PostUpgradeToPlus.tsx} | 57 +++++--- .../src/components/post/PostContent.tsx | 6 +- .../post/common/PostClickbaitShield.tsx | 17 ++- .../post/smartPrompts/PromptButtons.tsx | 135 ++++++++++++++++++ .../post/smartPrompts/SmartPrompt.tsx | 90 ++++++++++++ .../components/post/smartPrompts/common.ts | 22 +++ packages/shared/src/graphql/prompt.ts | 10 +- .../shared/src/hooks/feed/usePromptButtons.ts | 62 ++++++++ 9 files changed, 382 insertions(+), 32 deletions(-) rename packages/shared/src/components/plus/{ClickbaitTrial.tsx => PostUpgradeToPlus.tsx} (62%) create mode 100644 packages/shared/src/components/post/smartPrompts/PromptButtons.tsx create mode 100644 packages/shared/src/components/post/smartPrompts/SmartPrompt.tsx create mode 100644 packages/shared/src/components/post/smartPrompts/common.ts create mode 100644 packages/shared/src/hooks/feed/usePromptButtons.ts 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/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 b0141a9139..a7b1f40691 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(); @@ -61,7 +62,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/PromptButtons.tsx b/packages/shared/src/components/post/smartPrompts/PromptButtons.tsx new file mode 100644 index 0000000000..7b95e7a073 --- /dev/null +++ b/packages/shared/src/components/post/smartPrompts/PromptButtons.tsx @@ -0,0 +1,135 @@ +import React, { forwardRef, useState } from 'react'; +import type { ReactElement, Ref } from 'react'; +import { ColorName } from '../../../styles/colors'; +import { ArrowIcon, CustomPromptIcon } from '../../icons'; +import type { ButtonProps } from '../../buttons/Button'; +import { + Button, + ButtonIconPosition, + ButtonSize, + ButtonVariant, +} from '../../buttons/Button'; +import { IconSize } from '../../Icon'; +import { usePromptsQuery } from '../../../hooks/prompt/usePromptsQuery'; +import { ElementPlaceholder } from '../../ElementPlaceholder'; +import type { PromptFlags } from '../../../graphql/prompt'; +import { PromptDisplay } from '../../../graphql/prompt'; +import { usePromptButtons } from '../../../hooks/feed/usePromptButtons'; +import { useViewSize, ViewSize } from '../../../hooks'; +import { SimpleTooltip } from '../../tooltips'; +import { promptColorMap, PromptIconMap } from './common'; + +type PromptButtonProps = ButtonProps<'button'> & { + active: boolean; + flags: PromptFlags; +}; + +const PromptButton = forwardRef( + ( + { children, flags, active, ...props }: PromptButtonProps, + ref?: Ref, + ): ReactElement => { + const PromptIcon = PromptIconMap[flags.icon] || CustomPromptIcon; + const variant = active ? ButtonVariant.Primary : ButtonVariant.Subtle; + const color = active ? flags.color : undefined; + return ( + + ); + }, +); +PromptButton.displayName = 'PromptButton'; + +type PromptButtonsProps = { + activePrompt: string; + setActivePrompt: (prompt: string) => void; + width: number; +}; + +export const PromptButtons = ({ + activePrompt, + setActivePrompt, + width, +}: PromptButtonsProps): ReactElement => { + const isMobile = useViewSize(ViewSize.MobileL); + const [showAll, setShowAll] = useState(false); + const { data: prompts, isLoading } = usePromptsQuery(); + const promptList = usePromptButtons({ + prompts, + width, + offset: 82, + base: 16, + showAll: showAll || isMobile, + }); + + const promptsCount = prompts?.length || 0; + const remainingTags = promptsCount - promptList?.length; + + if (isLoading) { + return ( +
+ + + + + +
+ ); + } + + return ( +
+ setActivePrompt(PromptDisplay.TLDR)} + > + TLDR + + + {promptList?.map(({ id, label, flags, description }) => ( + + setActivePrompt(id)} + > + {label} + + + ))} + + {!showAll && !isMobile && ( + + + + )} +
+ ); +}; diff --git a/packages/shared/src/components/post/smartPrompts/SmartPrompt.tsx b/packages/shared/src/components/post/smartPrompts/SmartPrompt.tsx new file mode 100644 index 0000000000..342d1e4675 --- /dev/null +++ b/packages/shared/src/components/post/smartPrompts/SmartPrompt.tsx @@ -0,0 +1,90 @@ +import React, { useRef, useState } from 'react'; +import type { ReactElement } from 'react'; +import type { Post } from '../../../graphql/posts'; +import PostSummary from '../../cards/common/PostSummary'; +import { Tab, TabContainer } from '../../tabs/TabContainer'; +import { usePlusSubscription } from '../../../hooks'; +import { PromptButtons } from './PromptButtons'; +import { PromptDisplay } from '../../../graphql/prompt'; +import { PostUpgradeToPlus } from '../../plus/PostUpgradeToPlus'; +import { TargetId } from '../../../lib/log'; +import ShowMoreContent from '../../cards/common/ShowMoreContent'; + +export const SmartPrompt = ({ post }: { post: Post }): ReactElement => { + const { isPlus, showPlusSubscription } = usePlusSubscription(); + const [activeDisplay, setActiveDisplay] = useState( + PromptDisplay.TLDR, + ); + const [activePrompt, setActivePrompt] = useState(PromptDisplay.TLDR); + const elementRef = useRef(null); + const width = elementRef?.current?.getBoundingClientRect()?.width || 0; + + const onSetActivePrompt = (prompt: string) => { + setActivePrompt(prompt); + if (!isPlus && prompt !== PromptDisplay.TLDR) { + setActiveDisplay(PromptDisplay.UpgradeToPlus); + return; + } + + switch (prompt) { + case PromptDisplay.TLDR: + setActiveDisplay(PromptDisplay.TLDR); + break; + case PromptDisplay.CustomPrompt: + setActiveDisplay(PromptDisplay.CustomPrompt); + break; + default: + setActiveDisplay(PromptDisplay.SmartPrompt); + break; + } + }; + + if (!showPlusSubscription) { + return ; + } + + return ( +
+ + + + + + + + Smart prompt - {activePrompt} + + + + + + + + { + setActiveDisplay(PromptDisplay.TLDR); + }} + > + 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/post/smartPrompts/common.ts b/packages/shared/src/components/post/smartPrompts/common.ts new file mode 100644 index 0000000000..c97cba908b --- /dev/null +++ b/packages/shared/src/components/post/smartPrompts/common.ts @@ -0,0 +1,22 @@ +import { CustomPromptIcon, EditPromptIcon, TLDRIcon } from '../../icons'; + +export const PromptIconMap = { + TLDR: TLDRIcon, + CustomPrompt: CustomPromptIcon, + EditPrompt: EditPromptIcon, +}; + +export const promptColorMap = { + burger: 'text-accent-burger-default', + blueCheese: 'text-accent-blueCheese-default', + avocado: 'text-accent-avocado-default', + lettuce: 'text-accent-lettuce-default', + cheese: 'text-accent-cheese-default', + bun: 'text-accent-bun-default', + ketchup: 'text-accent-ketchup-default', + bacon: 'text-accent-bacon-default', + cabbage: 'text-accent-cabbage-default', + onion: 'text-accent-onion-default', + water: 'text-accent-water-default', + salt: 'text-accent-salt-default', +}; diff --git a/packages/shared/src/graphql/prompt.ts b/packages/shared/src/graphql/prompt.ts index 7afa890bdb..6764970f16 100644 --- a/packages/shared/src/graphql/prompt.ts +++ b/packages/shared/src/graphql/prompt.ts @@ -1,8 +1,9 @@ import { gql } from 'graphql-request'; +import type { ColorName } from '../styles/colors'; export type PromptFlags = { icon?: string; - color?: string; + color?: ColorName; }; export type Prompt = { @@ -14,6 +15,13 @@ export type Prompt = { flags?: PromptFlags; }; +export enum PromptDisplay { + TLDR = 'tldr', + UpgradeToPlus = 'upgrade-to-plus', + SmartPrompt = 'smart-prompt', + CustomPrompt = 'custom-prompt', +} + export const PROMPTS_QUERY = gql` query Prompts { prompts { diff --git a/packages/shared/src/hooks/feed/usePromptButtons.ts b/packages/shared/src/hooks/feed/usePromptButtons.ts new file mode 100644 index 0000000000..86e7ad4849 --- /dev/null +++ b/packages/shared/src/hooks/feed/usePromptButtons.ts @@ -0,0 +1,62 @@ +import { useMemo } from 'react'; +import type { Prompt } from '../../graphql/prompt'; + +interface UseFeedTags { + prompts: Prompt[]; + width: number; + base?: number; + offset?: number; + showAll?: boolean; +} + +const basePadding = 25; +const char = 8; +const gap = 8; + +export const usePromptButtons = ({ + base = basePadding, + prompts, + width, + offset = 0, + showAll = false, +}: UseFeedTags): Prompt[] => { + return useMemo(() => { + if (showAll) { + return prompts; + } + + if (!prompts?.length || width === 0) { + return []; + } + + let totalLength = offset; + + return prompts.reduce((items, tag, index) => { + const baseWidth = base + gap; + const minWidth = index === 0 ? base : baseWidth; + const addition = tag.label.length * char + minWidth; + const remaining = prompts.length - (items.length + 1); // the value 1 is for the tag we are about to add here + + totalLength += addition; + + if (remaining === 0) { + if (totalLength <= width) { + items.push(tag); + } + + return items; + } + + const remainingChars = remaining.toString().length * char; + const remainingWidth = baseWidth + remainingChars; + + if (totalLength + remainingWidth > width) { + return items; + } + + items.push(tag); + + return items; + }, []); + }, [showAll, prompts, width, offset, base]); +}; From fa8bc7fb5e22b15542ddbf31e775f717c99aadf2 Mon Sep 17 00:00:00 2001 From: Chris Bongers Date: Tue, 7 Jan 2025 11:34:50 +0200 Subject: [PATCH 07/14] fix: add custom prompt box (#4047) --- .../post/smartPrompts/SmartPrompt.tsx | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/packages/shared/src/components/post/smartPrompts/SmartPrompt.tsx b/packages/shared/src/components/post/smartPrompts/SmartPrompt.tsx index 342d1e4675..510712d2a3 100644 --- a/packages/shared/src/components/post/smartPrompts/SmartPrompt.tsx +++ b/packages/shared/src/components/post/smartPrompts/SmartPrompt.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useState } from 'react'; +import React, { useCallback, useRef, useState } from 'react'; import type { ReactElement } from 'react'; import type { Post } from '../../../graphql/posts'; import PostSummary from '../../cards/common/PostSummary'; @@ -9,6 +9,7 @@ import { PromptDisplay } from '../../../graphql/prompt'; import { PostUpgradeToPlus } from '../../plus/PostUpgradeToPlus'; import { TargetId } from '../../../lib/log'; import ShowMoreContent from '../../cards/common/ShowMoreContent'; +import { Button, ButtonSize, ButtonVariant } from '../../buttons/Button'; export const SmartPrompt = ({ post }: { post: Post }): ReactElement => { const { isPlus, showPlusSubscription } = usePlusSubscription(); @@ -19,6 +20,10 @@ export const SmartPrompt = ({ post }: { post: Post }): ReactElement => { const elementRef = useRef(null); const width = elementRef?.current?.getBoundingClientRect()?.width || 0; + const onSubmitCustomPrompt = useCallback((e) => { + e.preventDefault(); + }, []); + const onSetActivePrompt = (prompt: string) => { setActivePrompt(prompt); if (!isPlus && prompt !== PromptDisplay.TLDR) { @@ -68,7 +73,24 @@ export const SmartPrompt = ({ post }: { post: Post }): ReactElement => { - +
+