diff --git a/src/Apps/InfiniteDiscovery/InfiniteDiscoveryApp.tsx b/src/Apps/InfiniteDiscovery/InfiniteDiscoveryApp.tsx new file mode 100644 index 00000000000..ce27d851dee --- /dev/null +++ b/src/Apps/InfiniteDiscovery/InfiniteDiscoveryApp.tsx @@ -0,0 +1,401 @@ +import { + Button, + Checkbox, + Expandable, + Flex, + Input, + Label, + Separator, + Text, +} from "@artsy/palette" +import { sampleSize } from "lodash" +import React, { useEffect } from "react" + +const buildMetaphysicsQuery = ( + likedArtworks, + dismissedArtworks, + weight, + artworksCountForTaste = 3, + mltFields = [], + artworksLimit, +) => { + const likedArtworkIds = likedArtworks.map(artwork => artwork.id) + const dismissedArtworkIds = dismissedArtworks.map(artwork => artwork.id) + + const variables = { + osWeights: weight, + likedArtworkIds: + likedArtworkIds.length > artworksCountForTaste - 1 + ? likedArtworkIds.slice(-artworksCountForTaste) + : [], + excludeArtworkIds: + likedArtworkIds.length < artworksCountForTaste + ? [...likedArtworkIds, ...dismissedArtworkIds] + : // split artworks after the last artworksCountForTaste liked artwork + [ + ...likedArtworkIds.slice(0, -artworksCountForTaste), + ...dismissedArtworkIds, + ], + ...(mltFields.length > 0 && { mltFields }), + limit: artworksLimit, + } + + return { + query: ` + query InfiniteDiscoveryAppQuery($likedArtworkIds: [String], $excludeArtworkIds: [String], $osWeights: [Float], $mltFields: [String], $limit: Int) { + discoverArtworks( + osWeights: $osWeights, + likedArtworkIds: $likedArtworkIds, + excludeArtworkIds: $excludeArtworkIds, + mltFields: $mltFields, + limit: $limit + ) { + edges { + node { + id: internalID + title + artistNames + medium + image { + url + } + } + } + } + } + `, + variables, + } +} + +const request = async (url, opts) => { + try { + const options: RequestInit = { + method: opts.method || "POST", + headers: { + "Content-Type": "application/json", + "Cache-Control": "no-cache", + "X-Access-Token": window.sd.CURRENT_USER.accessToken, + "X-User-Id": window.sd.CURRENT_USER.id, + }, + body: opts.body, + } + + const response = await fetch(url, options) + + if (!response?.ok) { + throw new Error(`Response status: ${response.status}`) + } + + const json = await response.json() + return json + } catch (error) { + console.log("Error fetching data") + console.error(error.message) + } +} + +export const InfiniteDiscoveryApp = () => { + const [artworks, setArtworks] = React.useState([]) as any + const [likedArtworks, setLikedArtworks] = React.useState([]) as any + const [dismissedArtworks, setDismissedArtworks] = React.useState([]) as any + const [loading, setLoading] = React.useState(false) + const [mltWeight, setMltWeight] = React.useState(0.6) + const [knnWeight, setKnnWeight] = React.useState(0.4) + const [tasteArtworks, setTasteArtworks] = React.useState(3) + const [mltFields, setMltFields] = React.useState([ + "genes", + "materials", + "tags", + "medium", + ]) + const [artworksLimit, setArtworksLimit] = React.useState(5) + + const onLike = artwork => { + setLikedArtworks([...likedArtworks, artwork]) + } + + const onDismiss = artwork => { + setDismissedArtworks([...dismissedArtworks, artwork]) + } + + const handleCheckboxChange = field => { + setMltFields(prevFields => { + const updatedFields = prevFields.includes(field) + ? prevFields.filter(f => f !== field) + : [...prevFields, field] + return updatedFields + }) + } + + const initialArtworks = async () => { + const response = await request("https://metaphysics-staging.artsy.net/v2", { + body: JSON.stringify( + buildMetaphysicsQuery( + likedArtworks, + dismissedArtworks, + [0.6, 0.4], + tasteArtworks, + mltFields as any, + artworksLimit, + ), + ), + }) + + const artworks = response?.data?.discoverArtworks?.edges.map( + edge => edge.node, + ) + setArtworks(sampleSize(artworks, 10)) + setLoading(false) + } + + const submitSearch = async () => { + setLoading(true) + + // continue showing curated artworks if liked artworks are less than 3 + if (likedArtworks.length < 3) { + return initialArtworks() + } + + const response = await request("https://metaphysics-staging.artsy.net/v2", { + body: JSON.stringify( + buildMetaphysicsQuery( + likedArtworks, + dismissedArtworks, + [mltWeight, knnWeight], + tasteArtworks, + mltFields as any, + artworksLimit, + ), + ), + }) + + const artworks = response?.data?.discoverArtworks?.edges.map( + edge => edge.node, + ) + setArtworks(artworks) + setLoading(false) + } + + useEffect(() => { + initialArtworks() + }, []) // eslint-disable-line + + if (artworks.length === 0) { + return

Loading...

+ } + + return ( + + +
+
+ + Welcome to the Infinite Discovery Notebook! Like or dismiss artworks + to receive personalized recommendations tailored to your + preferences. + +
+ + + + + + + + + setMltWeight(Number.parseFloat(e.target.value)) + } + max={1} + min={0} + /> + + + + setKnnWeight(Number.parseFloat(e.target.value)) + } + max={1} + min={0} + /> + + + + + setTasteArtworks(Number.parseInt(e.target.value)) + } + /> + + Fields to consider for MLT + + {["genes", "materials", "tags", "medium"].map(field => ( + <> + + handleCheckboxChange(field)} + /> + + ))} + + + + + + + setArtworksLimit(Number.parseInt(e.target.value)) + } + /> + + + + +
+
+
+
+
+ + {artworks.map((artwork: any) => { + return ( + + ) + })} + + + + + Liked artworks: + + + {likedArtworks.map((artwork: any) => { + return ( + + ) + })} + + + Dismissed artworks: + + + {dismissedArtworks.map((artwork: any) => { + return ( + + ) + })} + + +
+ ) +} + +const Artwork = ({ onLike, onDismiss, viewed = false, artworkResource }) => { + const [artwork, setArtwork] = React.useState(artworkResource) as any + + const onLikeEvent = a => { + onLike(a) + setArtwork({ ...artwork, liked: true }) + } + + const onDismissEvent = id => { + onDismiss(id) + setArtwork({ ...artwork, dismissed: true }) + } + + return ( +
+ + {artwork?.title + +

+ {artwork?.title} +

+

+ {artwork?.artistNames} +

+

{artwork.medium}

+

{artwork.id}

+ {viewed || + (artwork.title !== "Artwork not available" && ( + <> + + + + ))} +
+ ) +} diff --git a/src/Apps/InfiniteDiscovery/infiniteDiscoveryRoutes.tsx b/src/Apps/InfiniteDiscovery/infiniteDiscoveryRoutes.tsx new file mode 100644 index 00000000000..b3f7a7279b2 --- /dev/null +++ b/src/Apps/InfiniteDiscovery/infiniteDiscoveryRoutes.tsx @@ -0,0 +1,26 @@ +import loadable from "@loadable/component" +import type { RouteProps } from "System/Router/Route" + +const InfiniteDiscoveryApp = loadable( + () => + import( + /* webpackChunkName: "infiniteDiscoveryBundle" */ "./InfiniteDiscoveryApp" + ), + { + resolveComponent: component => component.InfiniteDiscoveryApp, + }, +) + +export const infiniteDiscoveryRoutes: RouteProps[] = [ + { + path: "/infinite-discovery", + cacheConfig: { + force: true, + }, + serverCacheTTL: 0, + getComponent: () => InfiniteDiscoveryApp, + onClientSideRender: () => { + InfiniteDiscoveryApp.preload() + }, + }, +] diff --git a/src/System/Relay/middleware/cacheHeaderMiddleware.ts b/src/System/Relay/middleware/cacheHeaderMiddleware.ts index d75a42ab524..c340d44327f 100644 --- a/src/System/Relay/middleware/cacheHeaderMiddleware.ts +++ b/src/System/Relay/middleware/cacheHeaderMiddleware.ts @@ -12,6 +12,7 @@ interface CacheHeaderMiddlewareProps { } export const shouldSkipCDNCache = (req, user, ttl, url) => { + return true /** * The order of these checks is important. * We always want to skip the cache no matter what if any of: @@ -71,11 +72,6 @@ export const cacheHeaderMiddleware = (props?: CacheHeaderMiddlewareProps) => { case shouldSkipCDNCache(req, props?.user, ttl, url): { return { "Cache-Control": "no-cache" } } - case !!ttl: { - return { - "Cache-Control": `max-age=${ttl}`, - } - } default: { return {} } diff --git a/src/routes.tsx b/src/routes.tsx index 7579821e30d..31ad536d0fd 100644 --- a/src/routes.tsx +++ b/src/routes.tsx @@ -24,6 +24,7 @@ import { featureRoutes } from "Apps/Feature/featureRoutes" import { geneRoutes } from "Apps/Gene/geneRoutes" import { homeRoutes } from "Apps/Home/homeRoutes" import { identityVerificationRoutes } from "Apps/IdentityVerification/identityVerificationRoutes" +import { infiniteDiscoveryRoutes } from "Apps/InfiniteDiscovery/infiniteDiscoveryRoutes" import { institutionPartnershipsRoutes } from "Apps/InstitutionPartnerships/institutionPartnershipsRoutes" import { invoiceRoutes } from "Apps/Invoice/invoiceRoutes" import { jobsRoutes } from "Apps/Jobs/jobsRoutes" @@ -93,6 +94,7 @@ const ROUTES = buildAppRoutes([ geneRoutes, homeRoutes, identityVerificationRoutes, + infiniteDiscoveryRoutes, institutionPartnershipsRoutes, invoiceRoutes, jobsRoutes,