forked from crowetic/commerce
Compare commits
6 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
5304683150 | ||
|
0c3283f591 | ||
|
47a1c3eea1 | ||
|
d12fb77ef9 | ||
|
59a00fe9ea | ||
|
9c3cc1f8b6 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -36,3 +36,4 @@ yarn-error.log*
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
.env*.local
|
||||
|
6
.vscode/settings.json
vendored
6
.vscode/settings.json
vendored
@ -2,8 +2,8 @@
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"typescript.enablePromptUseWorkspaceTsdk": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll": true,
|
||||
"source.organizeImports": true,
|
||||
"source.sortMembers": true
|
||||
"source.fixAll": "explicit",
|
||||
"source.organizeImports": "explicit",
|
||||
"source.sortMembers": "explicit"
|
||||
}
|
||||
}
|
||||
|
@ -35,7 +35,9 @@ export default async function RootLayout({ children }: { children: ReactNode })
|
||||
return (
|
||||
<html lang="en" className={GeistSans.variable}>
|
||||
<body className="bg-neutral-50 text-black selection:bg-teal-300 dark:bg-neutral-900 dark:text-white dark:selection:bg-pink-500 dark:selection:text-white">
|
||||
<Navbar />
|
||||
<Suspense>
|
||||
<Navbar />
|
||||
</Suspense>
|
||||
<Suspense>
|
||||
<main>{children}</main>
|
||||
</Suspense>
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { AddToCart } from 'components/cart/add-to-cart';
|
||||
import Price from 'components/price';
|
||||
import Prose from 'components/prose';
|
||||
import { FitToCart } from 'lib/ai/fit-to-cart';
|
||||
import { Product } from 'lib/shopify/types';
|
||||
import { VariantSelector } from './variant-selector';
|
||||
|
||||
@ -24,8 +25,8 @@ export function ProductDescription({ product }: { product: Product }) {
|
||||
html={product.descriptionHtml}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<AddToCart variants={product.variants} availableForSale={product.availableForSale} />
|
||||
<FitToCart currentProduct={product} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
84
lib/ai/fit-to-cart.tsx
Normal file
84
lib/ai/fit-to-cart.tsx
Normal file
@ -0,0 +1,84 @@
|
||||
import { OpenAIStream, StreamingTextResponse } from 'ai';
|
||||
import { getCart } from 'lib/shopify';
|
||||
import { Product } from 'lib/shopify/types';
|
||||
import { unstable_cache } from 'next/cache';
|
||||
import { cookies } from 'next/headers';
|
||||
import OpenAI from 'openai';
|
||||
import { Suspense } from 'react';
|
||||
|
||||
async function getCartFromCookies() {
|
||||
const cartId = cookies().get('cartId')?.value;
|
||||
if (cartId) {
|
||||
return getCart(cartId);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function FitToCart({ currentProduct }: { currentProduct: Product }) {
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<FitToCartInternal currentProduct={currentProduct} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
async function FitToCartInternal({ currentProduct }: { currentProduct: Product }) {
|
||||
const pitch = await getPitch({ currentProduct });
|
||||
if (!pitch) return null;
|
||||
return <div className="mt-6 text-sm leading-tight dark:text-white/[60%]">{pitch}</div>;
|
||||
}
|
||||
|
||||
const fireworks = new OpenAI({
|
||||
baseURL: 'https://api.fireworks.ai/inference/v1',
|
||||
apiKey: process.env.FIREWORKS_API_KEY!
|
||||
});
|
||||
|
||||
function buildPrompt(prompt: string) {
|
||||
return prompt.split('\n').map((message) => ({
|
||||
role: 'user' as const,
|
||||
content: message
|
||||
}));
|
||||
}
|
||||
|
||||
export async function getPitch({ currentProduct }: { currentProduct: Product }) {
|
||||
const cart = await getCartFromCookies();
|
||||
if (!cart) return null;
|
||||
const products = cart.lines
|
||||
.filter((line) => line.merchandise.product.id !== currentProduct.id)
|
||||
.map((line) => `"${line.merchandise.product.title}"`);
|
||||
if (!products.length) return null;
|
||||
|
||||
const prompt = `Write a 30 word pitch for why a person who has ${products.join(
|
||||
' and '
|
||||
)} in their shopping cart should also purchase the "${currentProduct.title}"`;
|
||||
|
||||
const query = {
|
||||
model: 'accounts/fireworks/models/mistral-7b-instruct-4k',
|
||||
stream: true,
|
||||
messages: buildPrompt(prompt),
|
||||
max_tokens: 1000,
|
||||
temperature: 0.75,
|
||||
top_p: 1,
|
||||
frequency_penalty: 1
|
||||
} as const;
|
||||
|
||||
return unstable_cache(async () => {
|
||||
// Request the Fireworks API for the response based on the prompt
|
||||
const response = await fireworks.chat.completions.create(query);
|
||||
|
||||
// Convert the response into a friendly text-stream
|
||||
const stream = OpenAIStream(response);
|
||||
|
||||
// Respond with the stream
|
||||
const streamingResponse = new StreamingTextResponse(stream);
|
||||
let text = await streamingResponse.text();
|
||||
// Remove the quotes from the response tht the LLM sometimes adds.
|
||||
text = text.trim().replace(/^"/, '').replace(/"$/, '');
|
||||
return text;
|
||||
}, [
|
||||
JSON.stringify(query),
|
||||
'1.0',
|
||||
process.env.VERCEL_BRANCH_URL || '',
|
||||
process.env.NODE_ENV || ''
|
||||
])();
|
||||
}
|
@ -210,6 +210,10 @@ export async function createCart(): Promise<Cart> {
|
||||
return reshapeCart(res.body.data.cartCreate.cart);
|
||||
}
|
||||
|
||||
function revalidateCart(cartId: string) {
|
||||
revalidateTag(`cart-${cartId}`);
|
||||
}
|
||||
|
||||
export async function addToCart(
|
||||
cartId: string,
|
||||
lines: { merchandiseId: string; quantity: number }[]
|
||||
@ -222,6 +226,7 @@ export async function addToCart(
|
||||
},
|
||||
cache: 'no-store'
|
||||
});
|
||||
revalidateCart(cartId);
|
||||
return reshapeCart(res.body.data.cartLinesAdd.cart);
|
||||
}
|
||||
|
||||
@ -234,6 +239,7 @@ export async function removeFromCart(cartId: string, lineIds: string[]): Promise
|
||||
},
|
||||
cache: 'no-store'
|
||||
});
|
||||
revalidateCart(cartId);
|
||||
|
||||
return reshapeCart(res.body.data.cartLinesRemove.cart);
|
||||
}
|
||||
@ -250,6 +256,7 @@ export async function updateCart(
|
||||
},
|
||||
cache: 'no-store'
|
||||
});
|
||||
revalidateCart(cartId);
|
||||
|
||||
return reshapeCart(res.body.data.cartLinesUpdate.cart);
|
||||
}
|
||||
@ -258,8 +265,7 @@ export async function getCart(cartId: string): Promise<Cart | undefined> {
|
||||
const res = await shopifyFetch<ShopifyCartOperation>({
|
||||
query: getCartQuery,
|
||||
variables: { cartId },
|
||||
tags: [TAGS.cart],
|
||||
cache: 'no-store'
|
||||
tags: [TAGS.cart, `cart-${cartId}`]
|
||||
});
|
||||
|
||||
// Old carts becomes `null` when you checkout.
|
||||
|
@ -24,9 +24,11 @@
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^1.7.17",
|
||||
"@heroicons/react": "^2.0.18",
|
||||
"ai": "^2.2.33",
|
||||
"clsx": "^2.0.0",
|
||||
"geist": "^1.0.0",
|
||||
"next": "14.0.0",
|
||||
"next": "14.1.1-canary.27",
|
||||
"openai": "^4.26.0",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0"
|
||||
},
|
||||
|
628
pnpm-lock.yaml
generated
628
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user