diff --git a/packages/shared/src/features/interests/hooks/useCreateInterest.ts b/packages/shared/src/features/interests/hooks/useCreateInterest.ts new file mode 100644 index 0000000000..6240ad7ab9 --- /dev/null +++ b/packages/shared/src/features/interests/hooks/useCreateInterest.ts @@ -0,0 +1,30 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useAuthContext } from '../../../contexts/AuthContext'; +import { useToastNotification } from '../../../hooks/useToastNotification'; +import { generateQueryKey, RequestKey } from '../../../lib/query'; +import { createInterest } from '../../../graphql/interests'; + +export const useCreateInterest = ({ + onCreated, +}: { + onCreated?: (id: string) => void; +} = {}) => { + const { user } = useAuthContext(); + const { displayToast } = useToastNotification(); + const queryClient = useQueryClient(); + + const { isPending, mutateAsync } = useMutation({ + mutationFn: (query: string) => createInterest(query), + onSuccess: async (interest) => { + await queryClient.invalidateQueries({ + queryKey: generateQueryKey(RequestKey.Interests, user), + }); + onCreated?.(interest.id); + }, + onError: () => { + displayToast('Failed to create the interest. Please try again.'); + }, + }); + + return { isCreating: isPending, createInterest: mutateAsync }; +}; diff --git a/packages/shared/src/features/interests/hooks/useDeleteInterest.ts b/packages/shared/src/features/interests/hooks/useDeleteInterest.ts new file mode 100644 index 0000000000..9777e58690 --- /dev/null +++ b/packages/shared/src/features/interests/hooks/useDeleteInterest.ts @@ -0,0 +1,30 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useAuthContext } from '../../../contexts/AuthContext'; +import { useToastNotification } from '../../../hooks/useToastNotification'; +import { generateQueryKey, RequestKey } from '../../../lib/query'; +import { deleteInterest } from '../../../graphql/interests'; + +export const useDeleteInterest = ({ + onDeleted, +}: { + onDeleted?: () => void; +} = {}) => { + const { user } = useAuthContext(); + const { displayToast } = useToastNotification(); + const queryClient = useQueryClient(); + + const { isPending, mutateAsync } = useMutation({ + mutationFn: (id: string) => deleteInterest(id), + onSuccess: async () => { + await queryClient.invalidateQueries({ + queryKey: generateQueryKey(RequestKey.Interests, user), + }); + onDeleted?.(); + }, + onError: () => { + displayToast('Failed to delete the interest. Please try again.'); + }, + }); + + return { isDeleting: isPending, deleteInterest: mutateAsync }; +}; diff --git a/packages/shared/src/features/interests/hooks/useSendInterestCommand.ts b/packages/shared/src/features/interests/hooks/useSendInterestCommand.ts new file mode 100644 index 0000000000..b208e5170f --- /dev/null +++ b/packages/shared/src/features/interests/hooks/useSendInterestCommand.ts @@ -0,0 +1,26 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useAuthContext } from '../../../contexts/AuthContext'; +import { useToastNotification } from '../../../hooks/useToastNotification'; +import { generateQueryKey, RequestKey } from '../../../lib/query'; +import { sendInterestCommand } from '../../../graphql/interests'; + +export const useSendInterestCommand = (id: string) => { + const { user } = useAuthContext(); + const { displayToast } = useToastNotification(); + const queryClient = useQueryClient(); + + const { isPending, mutateAsync } = useMutation({ + mutationFn: (text: string) => sendInterestCommand({ id, text }), + onSuccess: async () => { + displayToast('The agent is working on it ✅'); + await queryClient.invalidateQueries({ + queryKey: generateQueryKey(RequestKey.InterestFindings, user, id), + }); + }, + onError: () => { + displayToast('Failed to send the command. Please try again.'); + }, + }); + + return { isSending: isPending, sendCommand: mutateAsync }; +}; diff --git a/packages/shared/src/features/interests/hooks/useUpdateInterest.ts b/packages/shared/src/features/interests/hooks/useUpdateInterest.ts new file mode 100644 index 0000000000..44521a765e --- /dev/null +++ b/packages/shared/src/features/interests/hooks/useUpdateInterest.ts @@ -0,0 +1,31 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useAuthContext } from '../../../contexts/AuthContext'; +import { useToastNotification } from '../../../hooks/useToastNotification'; +import { generateQueryKey, RequestKey } from '../../../lib/query'; +import type { UpdateInterestInput } from '../../../graphql/interests'; +import { updateInterest } from '../../../graphql/interests'; + +export const useUpdateInterest = (id: string) => { + const { user } = useAuthContext(); + const { displayToast } = useToastNotification(); + const queryClient = useQueryClient(); + + const { isPending, mutateAsync } = useMutation({ + mutationFn: (data: UpdateInterestInput) => updateInterest({ id, data }), + onSuccess: async () => { + await Promise.all([ + queryClient.invalidateQueries({ + queryKey: generateQueryKey(RequestKey.Interests, user, id), + }), + queryClient.invalidateQueries({ + queryKey: generateQueryKey(RequestKey.Interests, user), + }), + ]); + }, + onError: () => { + displayToast('Failed to update the interest. Please try again.'); + }, + }); + + return { isUpdating: isPending, updateInterest: mutateAsync }; +}; diff --git a/packages/shared/src/features/interests/queries.ts b/packages/shared/src/features/interests/queries.ts new file mode 100644 index 0000000000..af90248166 --- /dev/null +++ b/packages/shared/src/features/interests/queries.ts @@ -0,0 +1,47 @@ +import { queryOptions } from '@tanstack/react-query'; +import { + getInterest, + getInterestFindings, + getInterestPosts, + getInterests, +} from '../../graphql/interests'; +import { generateQueryKey, RequestKey } from '../../lib/query'; +import type { LoggedUser } from '../../lib/user'; + +export const interestsQueryOptions = (user?: Pick) => + queryOptions({ + queryKey: generateQueryKey(RequestKey.Interests, user), + queryFn: getInterests, + enabled: !!user?.id, + staleTime: 0, + }); + +export const interestQueryOptions = ( + id: string, + user?: Pick, +) => + queryOptions({ + queryKey: generateQueryKey(RequestKey.Interests, user, id), + queryFn: () => getInterest(id), + enabled: !!user?.id && !!id, + }); + +export const interestFindingsQueryOptions = ( + id: string, + user?: Pick, +) => + queryOptions({ + queryKey: generateQueryKey(RequestKey.InterestFindings, user, id), + queryFn: () => getInterestFindings(id), + enabled: !!user?.id && !!id, + }); + +export const interestPostsQueryOptions = ( + id: string, + user?: Pick, +) => + queryOptions({ + queryKey: generateQueryKey(RequestKey.InterestFindings, user, id, 'posts'), + queryFn: () => getInterestPosts(id), + enabled: !!user?.id && !!id, + }); diff --git a/packages/shared/src/graphql/interests.ts b/packages/shared/src/graphql/interests.ts new file mode 100644 index 0000000000..f2b6311048 --- /dev/null +++ b/packages/shared/src/graphql/interests.ts @@ -0,0 +1,252 @@ +import { gqlClient } from './common'; +import type { Post } from './posts'; + +export enum UserInterestStatus { + Active = 'active', + Paused = 'paused', + Stopped = 'stopped', +} + +export enum UserInterestCadence { + Hourly = 'hourly', + Daily = 'daily', + Weekly = 'weekly', +} + +export type InterestSources = { + dailyDev: boolean; + web: boolean; + github: boolean; +}; + +export type InterestOutputModes = { + feed: boolean; + post: boolean; + digest: boolean; + notification: boolean; +}; + +export type UserInterest = { + id: string; + query: string; + status: UserInterestStatus; + cadence: UserInterestCadence; + fomoThreshold: number; + sources: InterestSources; + outputModes: InterestOutputModes; + feedId?: string | null; + sourceId?: string | null; + lastRunAt?: string | null; + lastRunSummary?: string | null; + createdAt: string; + updatedAt: string; +}; + +export type UpdateInterestInput = { + status?: UserInterestStatus; + cadence?: UserInterestCadence; + fomoThreshold?: number; + sources?: Partial; + outputModes?: Partial; +}; + +export type InterestFinding = { + id: string; + postId: string; + score: number; + rationale?: string | null; + status: string; + createdAt: string; + post?: Pick< + Post, + 'id' | 'title' | 'image' | 'permalink' | 'commentsPermalink' | 'readTime' + > | null; +}; + +const USER_INTEREST_FRAGMENT = ` + fragment UserInterestFragment on UserInterest { + id + query + status + cadence + fomoThreshold + sources + outputModes + feedId + sourceId + lastRunAt + lastRunSummary + createdAt + updatedAt + } +`; + +export const INTERESTS_QUERY = ` + query Interests { + interests { + ...UserInterestFragment + } + } + ${USER_INTEREST_FRAGMENT} +`; + +export const INTEREST_QUERY = ` + query Interest($id: ID!) { + interest(id: $id) { + ...UserInterestFragment + } + } + ${USER_INTEREST_FRAGMENT} +`; + +export const INTEREST_FINDINGS_QUERY = ` + query InterestFindings($id: ID!) { + interestFindings(id: $id) { + id + postId + score + rationale + status + createdAt + post { + id + title + image + permalink + commentsPermalink + readTime + } + } + } +`; + +export const CREATE_INTEREST_MUTATION = ` + mutation CreateInterest($query: String!) { + createInterest(query: $query) { + ...UserInterestFragment + } + } + ${USER_INTEREST_FRAGMENT} +`; + +export const SEND_INTEREST_COMMAND_MUTATION = ` + mutation SendInterestCommand($id: ID!, $text: String!) { + sendInterestCommand(id: $id, text: $text) { + id + } + } +`; + +export const UPDATE_INTEREST_MUTATION = ` + mutation UpdateInterest($id: ID!, $data: UpdateInterestInput!) { + updateInterest(id: $id, data: $data) { + ...UserInterestFragment + } + } + ${USER_INTEREST_FRAGMENT} +`; + +export const DELETE_INTEREST_MUTATION = ` + mutation DeleteInterest($id: ID!) { + deleteInterest(id: $id) { + _ + } + } +`; + +export const INTEREST_POSTS_QUERY = ` + query InterestPosts($id: ID!) { + interestPosts(id: $id) { + id + title + content + contentHtml + permalink + commentsPermalink + createdAt + } + } +`; + +export type InterestPost = Pick< + Post, + | 'id' + | 'title' + | 'content' + | 'contentHtml' + | 'permalink' + | 'commentsPermalink' + | 'createdAt' +>; + +export const getInterests = async (): Promise => { + const res = await gqlClient.request<{ interests: UserInterest[] }>( + INTERESTS_QUERY, + ); + return res.interests; +}; + +export const getInterest = async (id: string): Promise => { + const res = await gqlClient.request<{ interest: UserInterest | null }>( + INTEREST_QUERY, + { id }, + ); + return res.interest; +}; + +export const getInterestFindings = async ( + id: string, +): Promise => { + const res = await gqlClient.request<{ interestFindings: InterestFinding[] }>( + INTEREST_FINDINGS_QUERY, + { id }, + ); + return res.interestFindings; +}; + +export const createInterest = async (query: string): Promise => { + const res = await gqlClient.request<{ createInterest: UserInterest }>( + CREATE_INTEREST_MUTATION, + { query }, + ); + return res.createInterest; +}; + +export const sendInterestCommand = async ({ + id, + text, +}: { + id: string; + text: string; +}): Promise> => { + const res = await gqlClient.request<{ + sendInterestCommand: Pick; + }>(SEND_INTEREST_COMMAND_MUTATION, { id, text }); + return res.sendInterestCommand; +}; + +export const updateInterest = async ({ + id, + data, +}: { + id: string; + data: UpdateInterestInput; +}): Promise => { + const res = await gqlClient.request<{ updateInterest: UserInterest }>( + UPDATE_INTEREST_MUTATION, + { id, data }, + ); + return res.updateInterest; +}; + +export const deleteInterest = async (id: string): Promise => { + await gqlClient.request(DELETE_INTEREST_MUTATION, { id }); +}; + +export const getInterestPosts = async (id: string): Promise => { + const res = await gqlClient.request<{ interestPosts: InterestPost[] }>( + INTEREST_POSTS_QUERY, + { id }, + ); + return res.interestPosts; +}; diff --git a/packages/shared/src/lib/featureManagement.ts b/packages/shared/src/lib/featureManagement.ts index 8ca5104a8e..034a155cd9 100644 --- a/packages/shared/src/lib/featureManagement.ts +++ b/packages/shared/src/lib/featureManagement.ts @@ -308,6 +308,8 @@ export const featureNotificationsRedesign = new Feature( // `false` — GrowthBook ramps it. export const featureCardImpressions = new Feature('card_impressions', false); +export const featureInterestAgent = new Feature('interest_agent', false); + // Post-signup feed activation bar: a persistent, non-dismissible strip shown // above the header on every page for signed-in users who registered but have // not set up their feed yet (no tag/content customization). Control hides it diff --git a/packages/shared/src/lib/query.ts b/packages/shared/src/lib/query.ts index 8c54cb2457..df66317958 100644 --- a/packages/shared/src/lib/query.ts +++ b/packages/shared/src/lib/query.ts @@ -181,6 +181,8 @@ export enum RequestKey { SquadStatus = 'squad_status', PublicSquadRequests = 'public_squad_requests', Feeds = 'feeds', + Interests = 'interests', + InterestFindings = 'interest_findings', ScheduledPosts = 'scheduled_posts', FeedSettings = 'feedSettings', Ads = 'ads', diff --git a/packages/webapp/pages/agent/[id].tsx b/packages/webapp/pages/agent/[id].tsx new file mode 100644 index 0000000000..79a94d59b3 --- /dev/null +++ b/packages/webapp/pages/agent/[id].tsx @@ -0,0 +1,453 @@ +import type { ReactElement } from 'react'; +import React, { useEffect, useState } from 'react'; +import type { NextSeoProps } from 'next-seo'; +import { useRouter } from 'next/router'; +import { format } from 'date-fns'; +import { + Typography, + TypographyType, + TypographyColor, +} from '@dailydotdev/shared/src/components/typography/Typography'; +import { + Button, + ButtonSize, + ButtonVariant, +} from '@dailydotdev/shared/src/components/buttons/Button'; +import { TextField } from '@dailydotdev/shared/src/components/fields/TextField'; +import { Slider } from '@dailydotdev/shared/src/components/fields/Slider'; +import { Switch } from '@dailydotdev/shared/src/components/fields/Switch'; +import { Dropdown } from '@dailydotdev/shared/src/components/fields/Dropdown'; +import Markdown from '@dailydotdev/shared/src/components/Markdown'; +import { Tooltip } from '@dailydotdev/shared/src/components/tooltip/Tooltip'; +import { DateFormat } from '@dailydotdev/shared/src/components/utilities/DateFormat'; +import { TimeFormatType } from '@dailydotdev/shared/src/lib/dateFormat'; +import { PageHeader } from '@dailydotdev/shared/src/components/layout/PageHeader'; +import { ArrowIcon } from '@dailydotdev/shared/src/components/icons'; +import { FlexCol, FlexRow } from '@dailydotdev/shared/src/components/utilities'; +import Link from '@dailydotdev/shared/src/components/utilities/Link'; +import { webappUrl } from '@dailydotdev/shared/src/lib/constants'; +import { useQuery } from '@tanstack/react-query'; +import { useConditionalFeature } from '@dailydotdev/shared/src/hooks/useConditionalFeature'; +import { useAuthContext } from '@dailydotdev/shared/src/contexts/AuthContext'; +import { featureInterestAgent } from '@dailydotdev/shared/src/lib/featureManagement'; +import { + UserInterestCadence, + UserInterestStatus, +} from '@dailydotdev/shared/src/graphql/interests'; +import { + interestQueryOptions, + interestFindingsQueryOptions, + interestPostsQueryOptions, +} from '@dailydotdev/shared/src/features/interests/queries'; +import { useSendInterestCommand } from '@dailydotdev/shared/src/features/interests/hooks/useSendInterestCommand'; +import { useUpdateInterest } from '@dailydotdev/shared/src/features/interests/hooks/useUpdateInterest'; +import { useDeleteInterest } from '@dailydotdev/shared/src/features/interests/hooks/useDeleteInterest'; +import { getLayout as getFooterNavBarLayout } from '../../components/layouts/FooterNavBarLayout'; +import { getLayout } from '../../components/layouts/MainLayout'; +import ProtectedPage from '../../components/ProtectedPage'; +import { getPageSeoTitles } from '../../components/layouts/utils'; + +const quickActions = ['Explore more', 'Write me a post']; +const quickActionHints: Record = { + 'Explore more': + 'Runs the agent now to hunt for more matching content. Note: the label is also saved to the refine history.', + 'Write me a post': + 'Runs the agent now and asks it to write a markdown summary post of what it found.', +}; +const sourceLabels: Record<'dailyDev' | 'web' | 'github', string> = { + dailyDev: 'daily.dev', + web: 'Web', + github: 'GitHub', +}; +const outputLabels: Record<'feed' | 'post' | 'notification', string> = { + feed: 'Feed', + post: 'Posts', + notification: 'Notifications', +}; +const cadenceOptions = [ + { value: UserInterestCadence.Hourly, label: 'Every hour' }, + { value: UserInterestCadence.Daily, label: 'Every day' }, + { value: UserInterestCadence.Weekly, label: 'Every week' }, +]; + +const CardTimestamp = ({ + date, +}: { + date?: string | null; +}): ReactElement | null => { + if (!date) { + return null; + } + + return ( + + + {`· ${format(new Date(date), 'HH:mm')}`} + + ); +}; + +const Page = (): ReactElement | null => { + const router = useRouter(); + const id = router.query.id as string; + const { user, isAuthReady } = useAuthContext(); + const { value: showAgent } = useConditionalFeature({ + feature: featureInterestAgent, + shouldEvaluate: isAuthReady, + }); + + const [feedback, setFeedback] = useState(''); + const [fomo, setFomo] = useState(null); + const interestQuery = useQuery(interestQueryOptions(id, user)); + const findingsQuery = useQuery(interestFindingsQueryOptions(id, user)); + const postsQuery = useQuery(interestPostsQueryOptions(id, user)); + const { isSending, sendCommand } = useSendInterestCommand(id); + const { isUpdating, updateInterest } = useUpdateInterest(id); + const { isDeleting, deleteInterest } = useDeleteInterest({ + onDeleted: () => router.push(`${webappUrl}agent`), + }); + + useEffect(() => { + if (isAuthReady && !showAgent) { + router.replace(webappUrl); + } + }, [isAuthReady, showAgent, router]); + + if (isAuthReady && !showAgent) { + return null; + } + + const interest = interestQuery.data; + const findings = findingsQuery.data ?? []; + const posts = postsQuery.data ?? []; + const isStopped = interest?.status === UserInterestStatus.Stopped; + + const onSendFeedback = () => { + const trimmed = feedback.trim(); + if (!trimmed || isSending) { + return; + } + sendCommand(trimmed); + setFeedback(''); + }; + + return ( + + + + + + + + + + + + + )} + + {interest && ( + + + Settings + + + + {`FOMO vs quality: ${(fomo ?? interest.fomoThreshold).toFixed( + 2, + )} (higher = only the best)`} + + setFomo(value)} + onValueCommit={([value]) => + updateInterest({ fomoThreshold: value }) + } + /> + + + + Cadence + + value === interest.cadence, + ), + )} + options={cadenceOptions.map(({ label }) => label)} + disabled={isUpdating || isStopped} + onChange={(_, index) => { + const cadence = cadenceOptions[index]?.value; + if (cadence && cadence !== interest.cadence) { + updateInterest({ cadence }); + } + }} + /> + + + + Sources + + {(['dailyDev', 'web', 'github'] as const).map((key) => ( + + updateInterest({ + sources: { [key]: !interest.sources?.[key] }, + }) + } + > + {sourceLabels[key]} + + ))} + + + + Outputs + + {(['feed', 'post', 'notification'] as const).map((key) => ( + + updateInterest({ + outputModes: { [key]: !interest.outputModes?.[key] }, + }) + } + > + {outputLabels[key]} + + ))} + + + )} + + + + Command the agent + + + {quickActions.map((action) => ( + + + + ))} + +
+ + + + +
+
+ + + + Posts + + {!postsQuery.isPending && !posts.length && ( + + No generated posts yet. + + )} + {posts.map((post) => ( + + + {post.title} + + + {post.contentHtml && } + + ))} + + + + + + + Feed + + + Sorted by relevance score + + + + + + + {findingsQuery.isPending && ( + Loading… + )} + {!findingsQuery.isPending && !findings.length && ( + + Nothing in your feed yet. The agent may still be hunting — try + Refresh in a moment. + + )} + {findings.map((finding) => { + const href = + finding.post?.commentsPermalink ?? finding.post?.permalink; + const title = finding.post?.title ?? finding.postId; + const body = ( + + + {title} + + + {`Score ${finding.score.toFixed(2)}`} + {finding.rationale ? ` · ${finding.rationale}` : ''} + + + + ); + + return href ? ( + + + {body} + + + ) : ( +
+ {body} +
+ ); + })} +
+ +
+ ); +}; + +const getAgentLayout: typeof getLayout = (...props) => + getFooterNavBarLayout(getLayout(...props)); + +const seo: NextSeoProps = { + ...getPageSeoTitles('Interest'), + nofollow: true, + noindex: true, +}; + +Page.getLayout = getAgentLayout; +Page.layoutProps = { seo, screenCentered: false }; + +export default Page; diff --git a/packages/webapp/pages/agent/index.tsx b/packages/webapp/pages/agent/index.tsx new file mode 100644 index 0000000000..da720e9594 --- /dev/null +++ b/packages/webapp/pages/agent/index.tsx @@ -0,0 +1,143 @@ +import type { ReactElement } from 'react'; +import React, { useEffect, useState } from 'react'; +import type { NextSeoProps } from 'next-seo'; +import { useRouter } from 'next/router'; +import { + Typography, + TypographyType, + TypographyColor, +} from '@dailydotdev/shared/src/components/typography/Typography'; +import { + Button, + ButtonSize, + ButtonVariant, +} from '@dailydotdev/shared/src/components/buttons/Button'; +import { TextField } from '@dailydotdev/shared/src/components/fields/TextField'; +import { PageHeader } from '@dailydotdev/shared/src/components/layout/PageHeader'; +import { FlexCol } from '@dailydotdev/shared/src/components/utilities'; +import Link from '@dailydotdev/shared/src/components/utilities/Link'; +import { webappUrl } from '@dailydotdev/shared/src/lib/constants'; +import { useQuery } from '@tanstack/react-query'; +import { useConditionalFeature } from '@dailydotdev/shared/src/hooks/useConditionalFeature'; +import { useAuthContext } from '@dailydotdev/shared/src/contexts/AuthContext'; +import { featureInterestAgent } from '@dailydotdev/shared/src/lib/featureManagement'; +import { interestsQueryOptions } from '@dailydotdev/shared/src/features/interests/queries'; +import { useCreateInterest } from '@dailydotdev/shared/src/features/interests/hooks/useCreateInterest'; +import { getLayout as getFooterNavBarLayout } from '../../components/layouts/FooterNavBarLayout'; +import { getLayout } from '../../components/layouts/MainLayout'; +import ProtectedPage from '../../components/ProtectedPage'; +import { getPageSeoTitles } from '../../components/layouts/utils'; + +const Page = (): ReactElement | null => { + const router = useRouter(); + const { user, isAuthReady } = useAuthContext(); + const { value: showAgent } = useConditionalFeature({ + feature: featureInterestAgent, + shouldEvaluate: isAuthReady, + }); + + const [query, setQuery] = useState(''); + const { data: interests, isPending } = useQuery(interestsQueryOptions(user)); + const { isCreating, createInterest } = useCreateInterest({ + onCreated: (id) => router.push(`${webappUrl}agent/${id}`), + }); + + useEffect(() => { + if (isAuthReady && !showAgent) { + router.replace(webappUrl); + } + }, [isAuthReady, showAgent, router]); + + if (isAuthReady && !showAgent) { + return null; + } + + const onSubmit = () => { + const trimmed = query.trim(); + if (!trimmed || isCreating) { + return; + } + createInterest(trimmed); + }; + + return ( + + +
+ + + Spawn an agent that hunts daily.dev for content on a topic you care + about. + +
+ + +
+
+ + + {isPending && ( + Loading… + )} + {!isPending && !interests?.length && ( + + No interests yet. Create your first one above. + + )} + {interests?.map((interest) => ( + + + + + {interest.query} + + + {interest.status} + {interest.lastRunSummary + ? ` · ${interest.lastRunSummary}` + : ''} + + + + + ))} + +
+
+ ); +}; + +const getAgentLayout: typeof getLayout = (...props) => + getFooterNavBarLayout(getLayout(...props)); + +const seo: NextSeoProps = { + ...getPageSeoTitles('Your interests'), + nofollow: true, + noindex: true, +}; + +Page.getLayout = getAgentLayout; +Page.layoutProps = { seo, screenCentered: false }; + +export default Page;